Programmstruktur für Mulithreading?

Heute ist Stammtischzeit:
Jeden Donnerstag 20:30 Uhr hier im Chat.
Wer Lust hat, kann sich gerne beteiligen. ;)
  • Hallo zusammen,

    auf Anregung von meigrafd stelle ich meine Frage [1] aus dem "Wie sage ich es in Python?"-Thread nochmal in einem eigenen Beitrag.
    In Ermangelung eines fertigen Codes habe ich nun mal 2 Stunden an einem Dummy gearbeitet:

    Ich möchte gerne folgendes probieren:

    • Die Funktion "add_tasks" wird ausgeführt und fügt einer (globalen) Warteschlange Aufgaben hinzu.
      Das wird später mal meine Berechnung für die Bewässerungsdauern. Ganz am Ende fügt sich die Funktion quasi nochmal selbst hinzu, damit wieder die Bewässerungsdauern für den nächsten Tag ausgerechnet werden. In die Queue geht ein Tupel aus Wartezeit, Funktionsname sowie ggf. Parametern.
    • Die beiden "open_valve" und "close_valve" Funktionen sind schlichte Aufgaben, welche nach Ablauf der Dauer ausgeführt werden.
    • Die Funktion "start_thread" soll den nächsten Eintrag aus der FIFO Warteschlange nehmen und die darin gewünschte Funktion in einem separaten Thread starten.


    Mein Hauptprogramm (insb. eine GUI) soll bedienbar bleiben. Daher meine Versuche mit diesen Threads.
    Allerdings kriege ich es nicht hin, dass immer der nächste Thread dann gestartet wird, wenn der eine gerade beendet wurde.


    Ich habe bisher nie mit Threads zu tun gehabt und vermute einen groben Verständnisfehler. Leider kann ich mit der Python-Doku auch nichts anfangen als Laie.
    Kann mir mal bitte jemand (ohne Code) mit Worten beschreiben in welche Richtung ich recherchieren muss und wie das vom Grundaufbau ausschauen müsste?

    Bzw, um auf die ursprüngliche Frage aus dem anderen Beitrag zurück zu kommen: muss es überhaupt Multi-Threading sein, oder geht das noch anders? (einfacher?)

    Vielen Dank.

    Gruß,
    Marcus Zurhorst


    [1] Wie sage ich es in Python ? (Einsteigerfragen)

    Einmal editiert, zuletzt von marcuszurhorst (10. August 2017 um 06:55)

  • Hallo,

    was an dem Code grundlegend falsch ist:

    * Importe in Funktionen - das macht man 1x am Anfang des Programms
    * exzessiver Gebrauch von globalen Variablen - das ist zu 99,9% immer falsch und in diesem Fall auf jeden Fall. Grund: der Zustand des Programm ist quasi nicht mehr nachvollziehbar und man kann Seiteneffekte bekommen, die unterwünscht sind.

    Letzteres müsstest du erst Mal in deinem Code eleminieren.

    Auch das sich `add_taksk`rekursiv aufruft ist komisch - dann kannst du auch direkt eine Endlosschleife programmieren, weil das auf's selbe hinausläuft.

    Es gibt übrigens auch fertige asynchorne Taksqueues für Python, da musst du das Rad nicht neu erfinden. Die bekanntest ist Celery, es gibt aber auch diverse Alternativen.

    Gruß, noisefloor

  • Du kannst uns auch unfertigen Code zeigen - besser du zeigst den Original Code denn dann entstehen auch keine Missverständnisse.

    So ganz verstehe ich das Problem nämlich noch nicht, weil der Zusammenhang mit deinem Beispielcode und dem Problem nicht zu erkennen ist, sehe aber schon einiges was ich anders machen würde...

    Was mir an deinem Beispielcode direkt auffällt: Du arbeitet leider sehr viel mit "global" aber das sollte man vermeiden. Übergebe zB "q" lieber direkt der Funktion. Besser wäre es so:
    [code=php]
    import time
    import queue
    import threading

    def add_tasks(q, startzeit):
    # Beispiel: 10 Sekunden, open_valve(1)
    q.put((10, open_valve, startzeit, 1))
    q.put((1, close_valve, 1))
    q.put((10, open_valve, startzeit, 2))
    q.put((1, close_valve, 2))

    def open_valve(startzeit, valvenum):
    print("{} Sekunden vergangen...".format(round(time.time()-startzeit, 1)))
    print(" Ventil {} geoeffnet.".format(valvenum))

    def close_valve(valvenum):
    print(" Ventil {} geschlossen.".format(valvenum))

    def start_thread(q):
    items = q.get()
    time = items[0]
    func = items[1]
    args = items[2:]
    print(time, func, args)
    t = threading.Timer(time, func, [args])
    t.start()

    #### Hauptprogramm ####

    startzeit = time.time()

    # Beim ersten Start die Queue anlegen und fuellen!
    tasks_queue = queue.Queue()
    add_tasks(tasks_queue, startzeit)

    print("Ab hier laufen die Schleifen")

    if not tasks_queue.empty():
    start_thread(tasks_queue)
    [/php]

    Aber wie gesagt, am besten zeigst du den original Code, damit sieht man das große ganze und worauf da zu achten wäre.

  • Ich glaube, Du hast da ein paar grundsätzliche Denkfehler drin. Was Du im Moment machst, ist folgendes:

    Du füllst am Anfang einmal die Queue mit allen Tasks. Dann rufst Du genau ein Mal start_thread() auf und startest damit genau einen Thread, der die erste Task nach der eingestellten Zeit startet. Mehr passiert dann nicht mehr.

    Würdest Du am Ende statt "if not q.empty():" "while not q.empty():" schreiben, würdest Du zwar für jede Task einen Thread starten - die Timer würden aber alle nahezu gleichzeitig anlaufen, d.h. nach 1 Sekunde würden alle Ventile geschlossen, nach weiteren 9 alle geöffnet.

    Ich habe hier mal einen Vorschlag für ein relativ simples Queueing mit Timestamps gemacht - vielleicht kannst Du das ja verwenden.

    Einmal editiert, zuletzt von Manul (10. August 2017 um 10:55)

  • Hallo zusammen!

    Erst mal vielen Dank. Leider habe ich bisher noch nicht den originalen Code, da ich ja erst in der Konzeptphase bin.
    Daher wollte ich ja mit dem Dummy arbeiten um das Thema zu verstehen.

    Mein Fehler ist wohl, dass ich für jede Aufgabe einen neuen Thread starten wollte und diese dann alle schön parallel laufen.
    Statt dessen muss ich genau 1 Thread parallel haben, und darin muss ich dann immer so lange warten, bis eine Aufgabe erledigt ist. Und dann kann ich mir dort eine neue Aufgabe aus der Warteschlange holen.
    Diesen Thread muss ich dann abbrechen, wenn z.B. im Hauptprogramm (per Klick auf Button) sagt, dass die Steuerung nicht mehr im Automatik-Modus, sondern im manuellen Betrieb ist.

    Ich habe hier einen neuen Dummy-Code. Der macht nun so ganz grob, wie ich mir das vorstelle.
    In Ermangelung eines GUI-Buttons habe ich einfach einen Zufallsgenerator verwendet, der mir irgendwann das Stop-Signal für den Thread sendet vom Hauptprogramm aus.
    Wenn die Warteschlange einmal abgearbeitet ist, wird sie automatisch neu befüllt und es geht weiter:


    Zu dem Hintergrund:
    Mein Programm läuft 24/7 mit einer GUI und idelt vor sich hin.
    Die Bewässerung erfolgt jede Nacht um z.B. 3:00 Uhr morgens und läuft nach folgendem Schema ab:
    1) Bewässerungsdauern für die Kreise ausrechnen und in Warteschlange einreihen
    2) Warteschlange abarbeiten (1 Wasserkreis nach dem anderen)
    3) Anschließend warten bis am nächsten Morgen wieder 3:00 Uhr erreicht ist.

    Für diesen letzten Punkt füge ich die add_tasks Funktion ans Ende der FIFO Schlange ein.
    Es ist im Prinzip schon eine Endlosschleife, nur dass eben fast ein Tag gewartet wird.


    Gruß,
    Marcus

  • Hallo,

    also wenn das ganze wirklich nur 1x pro Tag läuft, ist der Ansatz IMHO so wie so zu kompliziert.

    Dann ist es einfacher, wenn du 1x pro Tag dein Skript per systemd Timer Unit oder cron ausführen lässt. Das Skript könnte sich dann die Bewässungsdauer für die verschiedenen Segmente aus einer Config-Datei oder DB ziehen, die du halt täglich aktualisiert.

    Gruß, noisefloor

  • Hallo noisefloor,

    das wäre in der Tat das einfachste. Dann habe ich nur ein Arbeitsskript, und die GUI läuft unabhängig und speichert ihre Einstellungen in einem INI File, welches vom Skript 1x am Tag eingelesen wird.

    Aber: ich hatte es nicht explizit erwähnt, aber ich würde (natürlich) gerne z.B. so etwas wie einen Fortschrittsbalken im GUI animieren, der mir den aktuellen Task anzeigt.
    Das heißt vermutlich, dass es ohne Threads nicht gehen wird, gell?


    Was ist denn an meinem zweiten Dummy handwerklich falsch?


    Gruß,
    Marcus

  • Hallo,

    Zitat

    Das heißt vermutlich, dass es ohne Threads nicht gehen wird, gell?


    Das hat damit nicht direkt was zu tun. Dein GUI was nicht, was eine parallel laufende Funktion macht. Egal, ob die Funktion in einem Thread oder eine Prozess ausgeführt wird. Da musst du halt dafür sorgen, dass die Funktion mitteilt, was sie gerade macht. Dazu brauchst du dann z.B. eine weitere Queue oder sonst einen Mechanismus zum Nachrichten versenden.

    BTW: du sitzt doch nicht wirklich im 3 Uhr nachts vor dem Rechner und schaust einem Fortschrittsbalken zu, oder? ;)

    Gruß, noisefloor

  • Zitat von "noisefloor" pid='294907' dateline='1502389145'


    BTW: du sitzt doch nicht wirklich im 3 Uhr nachts vor dem Rechner und schaust einem Fortschrittsbalken zu, oder? ;)

    Auch das ist wieder ein gutes Argument. Aber wenn ich da so pragmatisch ran gehe, dann würde ich vermutlich nicht mal die Hälfte lernen. Ich suche meine Herausforderung, mir Aufgaben zu stellen und zu tüfteln bis es mir gefällt.

  • Richtig so :)

    Tob' dich mal an so einem Thema aus. Gerade im Bereich der GUI und speziell Ladebalken sind ein nettes Thema um eine Menge zu lernen :loading:

    Da ich die Schlangensprache zwar ganz gut verstehen kann, nicht aber schreiben, würde ich dir rein konzeptionell einfach schon mal den "Ratschlag" (hm, erinnert zu sehr an Ratgeber) da lassen, es einfach mal mit Callbacks zu probieren: Sprich du übergibst deiner (langandauernden Schleife) eine Funktion, die regelmäßig (bspw. nach jedem AddTask) ausgeführt wird. Diese Funktion initiiert dann das Update der GUI. Das hätte dann den Vorteil, dass du eine erste Implementierung hättest, die in der Regel auch synchron ablaufen könnte.

    Solltest du dann den Schritt weiter in Richtung Multithreading gehen wollen, dann ist Locking ein wichtiges Thema, denn leider kommt man da bei objektorientierter Programmierung und Parallelisierung nicht dran vorbei.

    Nur mal so am Rande als Motivatiönchen :)

    .NET-, Unity3D-, Web-Dev.
    Mikrocomputer-Hobbyist.

  • Da gäbe es (wie immer) mehrere Möglichkeiten. Da du auch noch eine GUI nutzt würde ich das evtl. getrennt behandeln, sprich, ein Hauptprogramm was die ganzen Sachen steuert und Aufgaben erledigt und ein zusätzliches GUI-Programm was sich dann via Socket/Queue nur einklinkt.

    Generell sollte man nicht einfach gnadenlos viele Threads haben, das macht es nicht wirklich einfacher und auch nicht schneller, eher im Gegenteil. Es gibt halt Multithreading und Multiprocessing... Man brauch eine gesunde Mischung.
    Ein weiterer wichtiger Teil ist das sog. IPC: Inter Process Communication, zum Austausch von Daten zwischen den Threads/Prozessen.
    Und zu guter letzt muss man nicht ständig einen Thread/Prozess starten, wieder beenden, wieder starten etc.. Da du sowieso Sachen nacheinander abhandeln willst wäre es IMHO besser dein "worker" bleibt permanent bestehen.

    Ich hatte dir ja schon 2 Links genannt:
    FAQ => Nützliche Links / Linksammlung => python: Schedule / mehrere Abhandlungen in einem Script / Parallelisierung
    FAQ => Nützliche Links / Linksammlung => python: mehrere Funktionen parallel laufen lassen und sauber beenden (multiprocessing)
    Hast du dir das mal angesehen?

  • Hallo meigrafd,

    ja, ich habe mir die Links bereits angesehen. Allerdings nicht so ganz verstanden auf Anhieb.
    Mir fehlt die Erfahrung, um mit „Socket/Queue“ oder „IPC“ mal ganz schnell auf die Lösung zu kommen, das sind alles vermutlich etablierte Standard-Modelle, die ich halt nicht kenne.

    Anyways:
    Den einen einzigen „Worker“, der permanent an bleibt, den habe ich ja jetzt schon.
    Diesen würde ich meiner Meinung nach über einen Button starten und wieder killen. (Button: Automatik-Modus; an = Worker-Thread starten // off = Worker-Thread beenden)
    [font="Arial"]
    Hattest du dir mein zweites Konstrukt mal angeschaut? Könnte das so klappen, wenn das noch etwas aufgehübscht wird?[/font]
    [font="Arial"]
    [/font]
    [font="Arial"]Gruß,[/font]
    [font="Arial"] Marcus[/font]


  • Hallo,

    Zitat

    Ich suche meine Herausforderung, mir Aufgaben zu stellen und zu tüfteln bis es mir gefällt.


    Grundsätzlich ok. Aber: wenn du aktuell nicht so viel Erfahrung hast, dann solltest du dein Programm Schritt-für-Schritt implementieren und nicht versuchen, direkt die eierlegende Wollmilchsau zu programmieren. Das geht nämlich ziemlich sicher schief und endet in Frust.
    Dann ist es besser, erst die Kernfunktionen zu implementieren (wie den Worker und das korrekte Abarbeiten der Task) und dann die nice-to-haves (wie eine Progess Bar).

    Zitat

    off = Worker-Thread beenden


    Threads haben unter Python OOTB keine Möglichkeit, extern beendet zu werden. Entweder der Thread wird von alleine fertig (was bei dir der Fall ist) oder läuft ewig - oder die implementierst einen Stop-Mechnismus selber.

    Übrigens bin ich immer noch der Meinung, dass sich für dich der Einsatz eine fertigen Lösung einer asynchronous task queue/job queue wie Celery oder Huey lohnt, anstatt alles selber zu implementieren.

    Gruß, noisefloor

  • Hallo marcuszurhorst,

    meigrafd und noisefloor haben Dir hier u.a. zwei sehr wichtige Hinweise gegeben. 1. zunächst auf das Kernproblem konzentrieren und 2. das Threading nicht zu übertreiben und nur soviel einzusetzen, wie wirklich nötig. Ich greife diese Gedanken auf, und frage: "Wozu überhaupt Multithreading bei einer Gartenbewässerung?".

    Warum nicht eine große Schleife, die:

    • nacheinander alle Bedingungen abprüft (z.B. die Sensoren der Bewässerungsstellen oder den Wetterbericht o.ä. abfragt),
    • dann eine Logik, die die jeweiligen Aktionen definiert (und diese ggf. in einer Queue speichert) ...
    • und eine Routine die diese Queue abarbeitet (z.B. die Aktoren bedient).
    • Jetzt folgen vielleicht irgendwelche Watchdog- oder sicherheitsrelevanten Dinge.
    • Pause und...
    • ... zurück auf Start.


    Multithreading und modulare Programmierung sind zwei orthogonale Dinge! Manchmal werden sie nur vermischt... Ob man wirklich Multithreading benötigt, erkennt man daran, ob der o.g. Schleifendurchlauf (modulo der expliziten Pause) so schnell ist, das der Rest des Systems nicht unter den Latenzen der Schleife leidet. Das könnte z.B. der Fall sein, wenn Du an irgendeinem Wetterdienst im Internet (synchron) Daten saugst. Das Abfragen von Sensoren oder Schalten von Ausgängen wird jedoch schwerlich das Bottleneck des Algorithmus bilden. Dann sollte man auch nicht parallelisieren, was nicht parallelisiert werden muß.

    Schöne Grüße und gutes Gelingen...

    schnasseldag

  • Hallo,

    Zitat

    Dann sollte man auch nicht parallelisieren, was nicht parallelisiert werden muß.


    +1 - zumal nebenläufig Programmierung nicht so trivial ist und am Anfang nicht so einfach zu verstehen ist. Verstehen im Sinne von "richtig & erfolgreich umsetzen".

    Zitat

    das der Rest des Systems nicht unter den Latenzen der Schleife leidet. Das könnte z.B. der Fall sein, wenn Du an irgendeinem Wetterdienst im Internet (synchron) Daten saugst.


    Das könnte man bei Python ab Version 3.5 mit dem asyncio Modul und `async def ...` und `await...` umgehen. Das Prinzip vom asyncio muss man aber auch erstmal verinnerlichen, was IMHO gerade am Anfang auch nicht wirklich trivial ist.

    Gruß, noisefloor

  • Hallo zusammen!

    Ihr habt wohl recht alle miteinander. Ich werde mich erst mal auf den Rest konzentrieren :thumbs1:
    Dieses Celery hatte mich abgeschreckt nachdem ich da gelesen habe dass ich auch noch einen "Broker" dazu programmieren müsste.


    Ich mach' das jetzt erst mal so:
    1) Der "Worker" ist ein eigenständiges Skript, welches ich über einen systemd Dienst jede Nacht starte.
    Dieser schaut prüft in der settings.ini Datei nach, ob er aktiv sein muss oder sich direkt wieder beendet.

    2) Total unabhängig davon läuft die GUI und schreibt eine Einstellung für den Automatik-Modus zurück in die setttings.ini


    Aber um das Thema abzuschließen:
    noisefloor, warum lässt ich ein Thread nicht abbrechen? -- Ist es nicht genau das, was ich bei meinem zweiten Versuch mache?
    --- Der Thread läuft beliebig lange bis der Zufallsgenerator aus dem Hauptprogramm das threading.Event.set() ausführt.

    Danke & Gruß,
    Marcus

  • Hallo,

    Zitat

    noisefloor, warum lässt ich ein Thread nicht abbrechen?


    Lässt sich schon - siehe oben. Was ich sagte ist, dass Python OOTB keine Methoden kennt, laufende Threads zu beendet. D.h. wenn du einen Thread hast, der z.B. einen Endlosschleife enthält, dann kann Python den mit Bordmitteln nicht beenden. Das musst du selber implementieren.
    Wenn der Thread ein Funktion enthält, die nach endlicher Zeit fertig ist, dann beendet sich dann auch der Thread.

    Gruß, noisefloor

  • Ich empfehle und nutze selber sehr gerne "beanstalkd" als Queue Server - ein unabhängiger, netzwerkfähiger Dienst wohin sich Scripts verbinden und wahlweise gemeinsam aufs gleiche oder unterschiedliche Queues zugreifen können. Siehe dazu Kommunikation zwischen zwei Python Programmen


    //EDIT: Hattest du eigentlich irgendwo schon erwähnt was du überhaupt als GUI verwendest? Mit Tkinter wäre es nämlich evtl. auch möglich auf zusätzliches Threading zu verzichten, da deine GUI ja ursprünglich permanent laufen sollte...

Jetzt mitmachen!

Du hast noch kein Benutzerkonto auf unserer Seite? Registriere dich kostenlos und nimm an unserer Community teil!