ubuntuusers.de

Python Threading für Anfänger

Status: Gelöst | Ubuntu-Version: Nicht spezifiziert
Antworten |

Apfelfrisch

Avatar von Apfelfrisch

Anmeldungsdatum:
12. August 2006

Beiträge: 652

Hi,

erstmal vorweg, ich bin grade angefangen Python zu lernen. Da meine Lernkurve ohne entsprechender Motivation eher gering ist, hab ich mir recht früh ein kleines Projekt gesucht. Also nehmt mich nicht gleich auseinander, ich werde wohl so ziemlich alle Regeln für unsauberen code eingehalten haben. (So lösche ich Beispielsweise wild wget-logs und kill prozesse;-)) Hier aber erstmal das Script

Was tut es, was soll es tun:
Mit diesem Script soll man über wget mehrer Downloads, welche in einer Textdatei liegen, parallel ausführen können. Da ich wegt im Hintergrund starte ist das downloaden auch erstmal nicht die Schwierigkeit. Um den Status des download anzuzeigen lese ich im 2 Sekundentakt die jeweils vorletzte Zeile des wgetlog aus. Das funktioniert mit 1 Download soweit auch ganz gut. Sobald ich allerdings mehrere Logdateien Parallel auslesen möchte bekomme ich Schwierigkeiten, da die Schleife zum auslesen ja noch mit der ersten Logdatei beschäftigt ist. Hier kommt IMHO das Threading ins spiel. Irgendwie möchte das bei mir aber nicht so richtig klappen, denn sobald ich die Funktion

print_log(link, zeile)


mittels

thread.start_new_thread(print_log,(link, zeile))


starte, bekomme ich überhaupt keine Ausgabe mehr.

Hat einer eine "einfache" Idee??

rocco_storm Team-Icon

Avatar von rocco_storm

Anmeldungsdatum:
3. November 2005

Beiträge: 1811

Wohnort: Ruhrpott

Guten morgen,

ich habe zwar grade keine Zeit deinen Quellcode nach Fehlern zu durchforsten, aber es gibt hier ein ganz gutes Beispiel zum Thema Threading, vielleicht bringt es dich ja voran.

Grüße,
r.

Apfelfrisch

(Themenstarter)
Avatar von Apfelfrisch

Anmeldungsdatum:
12. August 2006

Beiträge: 652

Habs mir wohl etwas zu einfach vorgestellt, nach meiner Logik müsste das

#!/usr/bin/python
import thread
import time

def print_count(count):
	for i in range(5):
		print "Hallo"+str(count)
		time.sleep(1)

thread.start_new_thread(print_count,(1,))


auch eine Ausgabe erzeugen, was es nicht tut 😉

Aber warum nicht

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4677

Wohnort: Berlin

@Apfelfrisch: Auf den ersten Blick: Da werden mehrere Threads gestartet, die regelmässig den Bildschirm löschen und dann mit ANSI-Escape-Codes formatierte Ausgaben vornehmen. Das ist keine gute Idee. Die laufen ja quasi gleichzeitig, d.h. die Reihenfolge in der das passiert ist willkürlich. Beispiel: Thread 1 löscht den Bildschirm und schreibt einen Teil seiner Ausgabe. Jetzt löscht Thread 2 den Bildschirm, und damit auch den ersten Teil der Ausgabe von Thread 1, und macht seine Ausgabe. Dann gibt Thread 1 wieder etwas aus und/oder löscht wieder den Bildschirm und zerstört damit die Ausgabe von Thread 2, und so weiter.

Umgehen kann man das zum Beispiel mit einem eigenen Thread für die Ausgabe der seine Daten per Queue.Queue bekommt. Damit das ganze übersichtlich bleibt, kommt man wohl auch nicht um OOP herum.

Dann wird der Thread erst nach dem Herunterladen gestartet. os.system ist ein blockierender Aufruf und es wird auch nicht die PID zurückgegeben, sondern der Exit Status des Programms das man gestartet hat.

Das Programm sieht insgesamt nicht besonders robust aus. Zum Beispiel werden Annahmen über die Logfile-Namen gemacht, die sehr fragwürdig sind. Es wäre wahrscheinlich besser wget über das subprocess-Modul zu starten und die Ausgabe direkt zu verarbeiten statt den Umweg über Logdateien zu gehen.

Ausserdem sollte man threading statt thread verwenden. Dein zweites Beispiel funktioniert nicht, weil das Hauptprogramm einen Thread startet und dann zuende ist, d.h. alle Threads sofort beendet werden. Entweder Du wartest explizit auf den Thread (join()-Methode) oder Du benutzt das threading-Modul, da endet das Programm erst wenn alle Threads fertig sind.

Apfelfrisch

(Themenstarter)
Avatar von Apfelfrisch

Anmeldungsdatum:
12. August 2006

Beiträge: 652

Das Script war auch eher als Übung gedacht, ob ich es jemals produktiv einsetzten werden wage ich zu bezweifeln. Nichts desto trotz danke für die Tipps. Mal schauen ob mich das Threadingmodul etwas weiter bringt.

Edit:
Hab es nun mal mit dem Codeschnipsel versucht, erhalte allerdings auch hier kein Ausgabe:

#!/usr/bin/python

import threading
import time

def print_count(count):
	for i in range(5):
		print "Hallo" + str(count)
      time.sleep(1)

threading._start_new_thread(print_count,(5,))

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4677

Wohnort: Berlin

Es wurden Tabs statt Leerzeichen zum Einrücken benutzt. Das kann schnell zu "unsichtbaren" Problemen führen wenn Tabs und Leerzeichen gemischt werden. Zum Beispiel sieht die Anzeige des Quelltextes mit anderen Einstellungen was denn nun ein Tab sein soll falsch aus. Das betrifft zum Beispiel Zeile 64 die nur mit Leerzeichen eingerückt ist. Empfehlung ist 4 Leerzeichen pro Ebene zu benutzen. Ebenfalls empfohlen ist ein Leerzeichen nach Kommata und vor und nach binären Operatoren.

Die read_parameter()-Funktion ist sehr eigenartig. Sie liesst eigentlich gar keinen Parameter und gibt entweder False oder eine Zeichenkette zurück, was eine sehr komische, unsaubere Mischung ist.

read_file() ist sehr umständlich und ineffizient. Dateien sind "iterable", das heisst man kann direkt über die Zeilen iterieren statt diese umständliche ``while``-Schleife zu verwenden. Und ein Tupel ist hier die falsche Datenstruktur, weil die nicht veränderbar ist. Darum dieses umständliche "hinzufügen", was jedesmal ein neues Tupel erzeugt, also den Inhalt des alten im Speicher kopiert nur um eine neue Zeile anzuhängen. Ausserdem wird die Datei in der Funktion nicht wieder geschlossen. Die einfachste Art eine Datei komplett in eine Liste einzulesen:

def read_file(filename):
    in_file = open(filename)
    result = list(in_file)
    in_file.close()
    return result

Es ist aber eigentlich gar nicht nötig die komplette Datei auf einen Schlag in den Speicher zu lesen, wenn man nur über die Zeilen iterieren möchte, weil man das wie gesagt direkt mit dem Dateiobjekt machen kann.

Auch print_log ist umständlich. Wenn man eine "do…while"/"do…until"-Schleife hat, wird das in Python idiomatisch als "Endlosschleife" mit einer ``if``-Abfrage und ``break`` zum verlassen der Schleife ausgedrückt. Dann muss man keinen Code mehrfach schreiben. Aus:

spam = get_something()
while condition(spam):
    do_something(spam)
    spam = get_something()

wird:

while True:
    spam = get_something()
    if not condition(spam):
        break
    do_something(spam)

Schöner ist es natürlich die ersten drei Zeilen des Schleifenkörpers in einem Iterator zu verpacken und in einer ``for``-Schleife zu verwenden.

Das ständige öffnen und schliessen der Datei ist unnötig. Die Bedingung sieht nicht sehr robust aus, falls sich die Ausgabe von ``wget`` mal ändern sollte. Statt das externe Programm ``clear`` aufzurufen könnte man auch dafür die entsprechedne ANSI-Sequenz ausgeben. Die ANSI-formatierten Ausgaben liessen sich per ``%`` kürzer und übersichtlicher schreiben:

        print '\033[%d;H%s' % (zeile * 3, link)
        print '\033[%d;H%s' % (zeile * 3 + 1, last_line)[/code]

Das ``else`` bei der ``while``-Schleife macht keinen Sinn.  Der Block sollte einfach nach der ``while``-Schleife stehen, eine Ebene weniger tief eingerückt.

Wenn eine Funktion nicht funktioniert wenn man kein vernünftiges Argument angibt, sollte man dieses Argument nicht mit einem ungültigen Wert vorbelegen.  Das Argument `str_file` bei `init_download()` kann/sollte nie die leere Zeichenkette sein.  An der ``if``/``else``-Konstruktion in dieser Funktion sieht man wie komisch der Rückgabewert von `read_parameter()` ist.

In Zeile 64 ist ein lustiger Fehler, der dafür sorgt, das `zeile` nach dem ersten Schleifendurchlauf immer 1 ist.  Statt selber hochzuzählen, kannst Du mal einen Blick auf die `enumerate()`-Funktion werfen.

Apfelfrisch

(Themenstarter)
Avatar von Apfelfrisch

Anmeldungsdatum:
12. August 2006

Beiträge: 652

Es wurden Tabs statt Leerzeichen zum Einrücken benutzt. Das kann schnell zu "unsichtbaren" Problemen führen wenn Tabs und Leerzeichen gemischt werden.

Verwende eigentlich Grundsätzlich nur Tabs, bei rüberkopieren ins Forum, ist allerdings was schief gelaufen.

Ich würde die von dir angesprochenen Fehler etc. gern erst einmal ignorieren, da das Script wahrscheinlich sowieso in der Tonne landet 😉 Allerdings würde vorher gerne wissen warum, das mit dem Threading nicht funktioniert.

Kannst du mir denn sagen, warum der oben gepostete Codeschnipsel (Post 5) keine Ausgabe erzeugt?

majotika

Avatar von majotika

Anmeldungsdatum:
13. August 2007

Beiträge: 142

Wohnort: Nürnberg

Apfelfrisch hat geschrieben:

Ich würde die von dir angesprochenen Fehler etc. gern erst einmal ignorieren, da das Script wahrscheinlich sowieso in der Tonne landet 😉

Jetzt hat er sich so eine Mühe gemacht und du haust ihm vorn Kopf.
Eines der wichtigsten Dinge am Programmieren, ist die Sauberkeit des Codes.
Also nimms dir bitte zu Herzen, was Marc 'BlackJack' Rintsch. Es gibt schon genug Menschen, die sich "Programmierer" nennen, es aber im Grunde genommen nicht sind.

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4677

Wohnort: Berlin

@Apfelfrisch: Du benutzt da eine Funktion mit einem Unterstrich am Anfang. Das heisst per Konvention die ist "privat" oder ein Implementierungsdetail und nicht für die Öffentlichkeit bestimmt. Wahrscheinlich ist das genau die gleiche Funktion wie thread.start_new_thread()¹. Du musst ein Thread-Objekt erstellen und dabei die Argumente target und args angeben und das Objekt dann start()en.

¹ Bei mir ist sie das jedenfalls:

In [333]: thread.start_new_thread is threading._start_new_thread
Out[333]: True

majotika

Avatar von majotika

Anmeldungsdatum:
13. August 2007

Beiträge: 142

Wohnort: Nürnberg

Apfelfrisch hat geschrieben:

Kannst du mir denn sagen, warum der oben gepostete Codeschnipsel (Post 5) keine Ausgabe erzeugt?

warum es nicht geht, kann ich auch nur vermuten.
dafür habe ich zu wenig Erfahrung in Sachen Python.
Aber ich habe mich auch mal dran versucht:

#!/usr/bin/env python

from threading import Thread
import time


class Zaehlklasse (Thread):   #deine Threadklasse
   def __init__(self, count): #musst du initialisieren
      Thread.__init__ (self)  #hier reicht die Methode der Threadklasse
      self.count = count      # Anzahl der Threads
   
   def run(self):             # die Methode, die beim Aufruf von start abgearbeitet wird
      print "Hallo" + str(self.count)
      time.sleep(1)
   

def main():
   anzahl = 5 # 5 Threads
   for i in range(anzahl):
      t = Zaehlklasse(i+1)
      t.start()   # starten
      t.join()    # hier muss ich mal warten, bis der einzelne Thread beendet ist,
                  # weil ich sonst Probleme mit TKinter kriege (print geht bei mir über graphische Konsole)
                  # es ist eben nicht gut, wenn 5 Threads gleichzeitig graphische Operationen machen wollen :D
                  # kann man aber weglassen, denk ich

   print "aus"


if __name__ == "__main__":
    main()

Apfelfrisch

(Themenstarter)
Avatar von Apfelfrisch

Anmeldungsdatum:
12. August 2006

Beiträge: 652

# hier muss ich mal warten, bis der einzelne Thread beendet ist,

Das war das Problem, ich habe nicht gewartet bis der Thread durchgelaufen ist. Danke nun funktioniert´s.

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4677

Wohnort: Berlin

Dann ist aber das Threading total überflüssig wenn am Ende doch alles schön der Reihe nach abgearbeitet wird. Das kann man auch einfacher haben. 🙄

majotika

Avatar von majotika

Anmeldungsdatum:
13. August 2007

Beiträge: 142

Wohnort: Nürnberg

das versteh ich jetzt auch nicht ganz

na hauptsache, geholfen ☺ und wieder mal bissl Python geübt 😀

Apfelfrisch

(Themenstarter)
Avatar von Apfelfrisch

Anmeldungsdatum:
12. August 2006

Beiträge: 652

Dann ist aber das Threading total überflüssig wenn am Ende doch alles schön der Reihe nach abgearbeitet wird.

Wird es ja nicht, die Prozesse laufen schon parallel. Das Programm wartet halt bis nur bis alle Thread durchgelaufen sind bevor es beendet wird.
Läuft jetzt momentan in etwa so( Quellcode grad nicht da).

schleife:
Threadaufruf
tulpen =+ threadaufruf

schleife:
tulpen.join()

exit

Das kann man auch einfacher haben. 🙄

Das ist gut möglich, nur weiss ich nicht wie 😉
Das ganze Script ist wie gesagt nur dazu gedacht gewesen Python zu lernen. Du hasst es oben ja auch schon gut auseinander genommen. Da es aber erstmal läuft bin ich glücklich. Mal sehen ob ich mich noch einmal damit beschäftige, wenn ich mich in Klassen etwas eingearbeitet habe.

Antworten |