Python und Threading

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Sneedlewoods
User
Beiträge: 2
Registriert: Sonntag 8. September 2013, 13:38

Hallo Community,

ich habe heute meine ersten Python-Schritte unternommen und muss sagen, dass es eine Sprache ist, nach der ich schon lange gesucht habe. In C# hab ich häufig stundenlang nach irgendwelchen Krams gegoogelt, der mit dem eigentlichen Problem rein gar nichts zu tun hatte.

Nun bin ich bei meinen ersten Schritten und Threading auf folgendes Verhalten gestoßen, wobei ich gerne auf Eure Erfahrung oder Wissen zurückgreifen möchte.

In meinem Programm versetze ich einen Punkt zufällig nach oben, unten, rechts, links was dann wie ein zitternder Punkt aussieht. Zusätzlich zeigt das Programm die Anzahl dieser Aktualisierungen/Punktdarstellung pro Sekunde an.

Die "Punktdarstellung" lief zunächst im Hauptprogramm unter einer "while True"-Schleife und zeigte über 3000 Punkte pro Sekunde an, wobei ich schon nicht schlecht gestaunt habe. Der Punkt wurde so schnell aktualisiert, dass man den Eindruck hatte, da tanzen 2-3 Punkte über den Bildschirm.

Die "Frame-Anzeige" habe ich mit einem Thread umgesetzt. Nun dachte ich, ich packe die Punktdarstellung auch in einen Thread, doch plötzlich werden die Punkte nur noch ca. 200 Mal pro Sekunde aktualisiert.

Nun die Frage, woran liegt das? Ist der Hauptthread normal soviel schneller? Oder liegt es daran, das der Thread nun eine weitere "while True"-Schleife besitzt?

Code: Alles auswählen

import time
import sys
import random
#import and init pygame
import pygame
pygame.init()
#threading
from threading import Thread


#create the screen
window = pygame.display.set_mode((640, 480)) 

#festgelegte/globale Werte
MaxX = 639
MaxY = 479
Frames = 0
oldFrames = 0;

#pixel startposition
x = random.randint(0,MaxX)
y = random.randint(0,MaxY)

#Berechnet die neue Pixelposition
def newpos():
    z = random.randint(1, 4)
    global y
    global x
    if z == 1:
        y = y - 1
        if y < 0:
            y = MaxY
    if z == 2:
        x = x + 1
        if x > 639:
            x = 0;
    if z == 3:
        y = y + 1
        if y > 479:
            y = 0
    if z == 4:
        x = x - 1
        if x < 0:
            x = MaxX

#schreibt Text auf die Grafikanzeige
def textout(fps,r,g,b):
    global MaxX
    global MaxY
    font=pygame.font.SysFont("arial",30)
    text=font.render(str(fps), 1,(r,g,b))
    window.blit(text, (MaxX-100, MaxY-30))

#Thread zur Frame-Anzeige
def showfps():
    global Frames
    global oldFrames
    textout(oldFrames,0,0,0)
    FrameStamp = Frames
    textout(FrameStamp,255,255,255)
    Frames = 0
    oldFrames = FrameStamp
    time.sleep(1)
    showfps()

#Thread Punktdarstellung
def DrawPoint():
    global x
    global y
    global Frames
    while True:
        pygame.draw.line(window, (0,0,0), (x,y), (x+1,y))
        newpos()
        pygame.draw.line(window, (255,255,255), (x,y), (x+1,y))
        pygame.display.flip()
        Frames = Frames + 1

#Thread Frame-Anzeige starten
t1 = Thread(target=showfps)
t1.daemon = True
t1.start()

#Thread Punktdarstellung starten
t2 = Thread(target=DrawPoint)
t2.daemon = True
t2.start()

#input handling
while True:
    for event in pygame.event.get(): 
        if event.type == pygame.QUIT: 
            sys.exit(0)
        else: 
            print (event)
Grüßerle Sneedle
BlackJack

@Sneedlewoods: Das dürfte in CPython hauptsächlich daran liegen, dass Python-Bytecode immer nur von einem Thread gleichzeitig ausgeführt wird. Die anderen Threads warten solange und machen gar nichts. Das sieht in anderen Python-Implementierungen zum Teil anders auch, aber in CPython kann man Threads nicht wirklich dazu verwenden um Geschwindigkeit beim Parallelisieren von Code zu erreichen.
Sneedlewoods
User
Beiträge: 2
Registriert: Sonntag 8. September 2013, 13:38

Hi BlackJack,

danke für Deinen Hinweis. Bin nach ein wenig Recherche auf das Modul "multiprocessing" gestoßen, welches wohl dafür besser sein soll. Mal schauen.
Für die, die es interessiert, wird der Performance-Einbruch durch GIL (Global Interpreter Lock) verursacht. Python ist wohl darauf ausgelegt als Single-Thread zu laufen.

Grüße Sneedle
BlackJack

@Sneedlewoods: `multiprocessing` wird Dir hier nichts nützen denn das nutzt wie der Name schon sagt verschiedene Prozesse, also verschiedene Adressräume. Und das Python auf Single-Thread ausgelegt ist kann man auch nicht sagen. Zum einen ist das GIL ein Implementierungsdetail was nicht jede Python-Implementierung besitzt, zum anderen kann man auch mit GIL sinnvoll Threads einsetzen, nur halt nicht zum parallelisieren von rechenintensivem Code der hauptsächlich aus der Abarbeitung von Python-Bytecode besteht.

Noch ein paar Anmerkungen zum Quelltext:

Kommentare sollten dem Leser einen Mehrwert bieten und nicht einfach noch mal wiederholen was im Quelltext schon offensichtlich steht. Also so etwas wie ``#import and init pygame`` vor einem ``import`` von `pygame` und dem Aufruf der `init()`-Funktion ist total überflüssig. Wenn man kommentieren muss *was* im Quelltext gemacht wird, ist das oft ein Zeichen, dass man den Quelltext verständlicher schreiben kann. Kommentare sind für Erklärungen *warum* etwas so gemacht wird, wie der Quelltext es beschreibt, also Informationen die nicht aus dem Quelltext selber hervorgehen.

Bezüglich der Namensschreibweise könntest Du mal einen Blick in den Style Guide for Python Code werfen.

Auf Modulebene sollten nur Konstante Werte, Module, Funktionen, und Klassen stehen. Wenn man dort keine Variablen ablegt, braucht man auch kein ``global``, was in sauberen Programmen nichts verloren hat. Werte sollten Funktionen als Argumente betreten und als Rückgabewerte verlassen, nur dann sind es auch wirklich in sich abgeschlossene Funktionen und nicht einfach nur benannte Code-Abschnitte die undurchsichtig über globale Variablen verbunden sind.

Das Hauptprogramm sollte in einer Funktion verschwinden, meistens `main()` genannt, und über das ``if __name__ == '__main__': main()``-Idiom gestartet werden. Dann kann man das Modul auch importieren und wiederverwenden oder einzelne Teile (Funktionen, Klassen) ausprobieren ohne dass das Hauptprogramm los läuft. Das Hauptprogramm ist dann auch nicht über das ganze Modul verteilt und von Funktionen unterbrochen und damit übersichtlicher.

Die Kommentare über den Funktionen wären besser als DocStrings in den Funktionen aufgehoben.

Namen von Funktionen sollten die Tätigkeit beschreiben die sie durchführen. Also statt `newposition()`, was eher der Name für den Wert der neuen Position ist, so etwas wie `calculate_new_position()`. In der Funktion werden mal die Konstanten für die maximalen Bildschirmkoordinaten verwendet und mal der jeweiligen Werte als Zahlen. Es sollten nur die Konstanten verwendet werden. Die Funktion lässt sich auch wesentlich einfacher mit dem Modulo-Operator und der zufälligen Auswahl von relativen Koordinaten ausdrücken. Damit wird die Funktion zum zweizeiler.

Die Funktionen bei denen ``Thread`` im Kommentar steht sind im Grunde keine Threads sondern Funktionen die *in* Threads ausgeführt werden.

`showfps()` ruft sich am Ende selbst auf. Diese Endlosrekursion wird zu einem Programmabbruch führen, da Python nicht nur keine „tail call optimization” garantiert, sondern viele Implementierungen sogar ein recht kleines Rekursionslimit haben welches nach einer bestimmten Anzahl von rekursiv verschachtelten Aufrufen zu einer Ausnahme führt. Damit kann man sagen die Funktion ist fehlerhaft.

Ebenfalls als Programmfehler würde ansehen, dass zwei Threads auf den Framezähler zugreifen, ohne dass diese Zugriffe gegeneinander geschützt sind. Auch wenn in CPython dabei eher nichts passieren wird, ist das äusserst unsauber. Gleiches gilt für verändernde Zugriffe auf Pygame-Objekte. Solange nicht klar ist, dass die thread-safe sind, sollte man das besser nicht machen.

An dieser Stelle würde ich mal das ganze Programm in der Form in Frage stellen. Es hat schon einen Grund warum GUIs und ähnliche Rahmenwerke mit einer Ereignisschleife arbeiten und nicht mit Threads.

Semikolons am Zeilenende sind unnötig.

Dann kommt man ungefähr auf das hier:

Code: Alles auswählen

#!/usr/bin/env python
import random
import sys
import time
from threading import Lock, Thread

import pygame

pygame.init()

SCREEN_WIDTH = 640
SCREEN_HEIGHT = 480

BLACK, WHITE = (0, 0, 0), (255, 255, 255)


def calculate_new_position(x, y):
    """Berechnet die neue Pixelposition."""
    delta_x, delta_y = random.choice([(0, 1), (0, -1), (1, 0), (-1, 0)])
    return (x + delta_x) % SCREEN_WIDTH, (y + delta_y) % SCREEN_HEIGHT


def blit_text(surface, text, colour, font=pygame.font.SysFont('arial', 30)):
    """Schreibt Text auf die Grafikanzeige."""
    surface.blit(
        font.render(str(text), 1, colour),
        (SCREEN_WIDTH - 100, SCREEN_HEIGHT - 30)
    )


class FrameCounter(object):
    def __init__(self, value=0):
        self.lock = Lock()
        self._value = value

    @property
    def value(self):
        with self.lock:
            return self._value

    def count_frame(self):
        with self.lock:
            self._value += 1


def update_fps(surface, frame_counter):
    old_frame_value = frame_counter.value
    while True:
        blit_text(surface, str(old_frame_value), BLACK)
        frame_value = frame_counter.value - old_frame_value
        blit_text(surface, str(frame_value), WHITE)
        old_frame_value = frame_value
        time.sleep(1)


def draw_point(surface, x, y, frame_counter):
    while True:
        pygame.draw.line(surface, BLACK, (x, y), (x + 1, y))
        x, y = calculate_new_position(x, y)
        pygame.draw.line(surface, WHITE, (x, y), (x + 1, y))
        pygame.display.flip()
        frame_counter.count_frame()


def main():
    window = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) 

    frame_counter = FrameCounter()

    fps_thread = Thread(target=update_fps, args=(window, frame_counter))
    fps_thread.daemon = True
    fps_thread.start()

    x = random.randrange(0, SCREEN_WIDTH)
    y = random.randrange(0, SCREEN_HEIGHT)
    point_thread = Thread(target=draw_point, args=(window, x, y, frame_counter))
    point_thread.daemon = True
    point_thread.start()

    while True:
        for event in pygame.event.get(): 
            if event.type == pygame.QUIT:
                sys.exit(0)
            else: 
                print event


if __name__ == '__main__':
    main()
Wo ich wie gesagt immer noch das Problem sehe, dass von mehreren Threads aus auf Pygame und damit SDL-Surfaces zugegriffen wird. Das würde ich nicht tun.
Antworten