Threading in Python [geschlossen]

76

Mit welchen Modulen werden Multithread-Anwendungen in Python geschrieben? Ich bin mir der grundlegenden Parallelitätsmechanismen bewusst, die von der Sprache und auch von Stackless Python bereitgestellt werden , aber was sind ihre jeweiligen Stärken und Schwächen?

Jon
quelle
Ich sehe mindestens drei Gründe dafür, dass diese Frage (Stellungnahme, Empfehlung und zu weit gefasst) auf Eis gelegt wird.
lpapp
Ich weiß, dass diese Frage von 09 ist, aber kann jemand sie asynciobitte mit beantworten ? Vielen Dank im Voraus und würde gerne sehen, wie der Code aussehen würde.
Alvas
@lpapp, wo die Frage einige meinungsgebundene Antworten hervorgebracht hat, glaube ich, dass der Wortlaut objektiv ist (vielleicht zu weit gefasst). Das OP fragt, welche Module in Python für die Parallelität verwendet werden und welche Vor- und Nachteile sie jeweils haben. Klingt für mich vernünftig.
Guyarad

Antworten:

116

In der Reihenfolge zunehmender Komplexität:

Verwenden Sie das Einfädelmodul

Vorteile:

  • Es ist wirklich einfach, jede Funktion (jede tatsächlich aufrufbare) in einem eigenen Thread auszuführen.
  • Das Teilen von Daten ist nicht einfach (Sperren sind nie einfach :), zumindest einfach.

Nachteile:

  • Wie von Jürgen Python erwähnt, können Threads nicht gleichzeitig auf den Status im Interpreter zugreifen (es gibt eine große Sperre, die berüchtigte globale Interpreter-Sperre) . In der Praxis bedeutet dies, dass Threads für E / A-gebundene Aufgaben (Netzwerk, Schreiben auf Festplatte, nützlich) nützlich sind. und so weiter), aber überhaupt nicht nützlich für die gleichzeitige Berechnung.

Verwenden Sie das Multiprozessor- Modul

Im einfachen Anwendungsfall sieht dies genauso aus wie mit, threadingaußer dass jede Aufgabe in einem eigenen Prozess ausgeführt wird, nicht in einem eigenen Thread. (Fast wörtlich: Wenn Sie nehmen Elis Beispiel , und ersetzen threadingmit multiprocessing, Threadmit Process, und Queue(das Modul) mit multiprocessing.Queue, sollte es ganz gut laufen.)

Vorteile:

  • Tatsächliche Parallelität für alle Aufgaben (keine globale Interpreter-Sperre).
  • Skaliert auf mehrere Prozessoren, kann sogar auf mehrere Maschinen skaliert werden .

Nachteile:

  • Prozesse sind langsamer als Threads.
  • Der Datenaustausch zwischen Prozessen ist schwieriger als bei Threads.
  • Der Speicher wird nicht implizit gemeinsam genutzt. Sie müssen es entweder explizit freigeben oder Variablen auswählen und hin und her senden. Das ist sicherer, aber schwieriger. (Wenn es zunehmend darauf ankommt, scheinen die Python-Entwickler die Leute in diese Richtung zu drängen.)

Verwenden Sie ein Ereignismodell wie Twisted

Vorteile:

  • Sie haben eine äußerst genaue Kontrolle über die Priorität und darüber, was wann ausgeführt wird.

Nachteile:

  • Selbst mit einer guten Bibliothek ist asynchrone Programmierung normalerweise schwieriger als Thread-Programmierung, sowohl hinsichtlich des Verständnisses, was passieren soll, als auch hinsichtlich des Debuggens, was tatsächlich passiert.

In allen Fällen gehe ich davon aus, dass Sie bereits viele Probleme im Zusammenhang mit Multitasking verstanden haben, insbesondere die schwierige Frage, wie Daten zwischen Aufgaben ausgetauscht werden können. Wenn Sie aus irgendeinem Grund nicht wissen, wann und wie Sie Sperren und Bedingungen verwenden sollen, müssen Sie mit diesen beginnen. Multitasking-Code ist voller Feinheiten und Fallstricke, und es ist wirklich am besten, ein gutes Verständnis der Konzepte zu haben, bevor Sie beginnen.

Quark
quelle
4
Ich würde argumentieren, dass Ihre Komplexitätsreihenfolge fast vollständig rückwärts ist. Multithread-Programmierung ist wirklich schwer korrekt durchzuführen (fast niemand tut dies). Event-Programmierung ist anders, aber es ist wirklich einfach zu verstehen, was los ist, und Tests zu schreiben, die beweisen, dass es das tut, was es sollte. (Ich sage, ich habe an diesem Wochenende eine 100% ige Abdeckung einer massiv gleichzeitigen Netzwerkbibliothek erreicht).
Dustin
3
Hmm. Ich denke, Event-Programmierung macht die Komplexität sichtbar. Es zwingt Sie, direkter damit umzugehen. Sie können argumentieren, dass die Komplexität der Parallelität inhärent ist, unabhängig davon, wie Sie sich ihr nähern, und ich würde Ihnen zustimmen. Aber nachdem ich einige ziemlich große Thread- und ereignisbasierte Programme erstellt habe, glaube ich, dass ich zu dem stehe, was ich gesagt habe: Das ereignisbasierte Programm war viel mehr unter meiner Kontrolle, aber es war komplexer, es tatsächlich zu codieren.
Quark
1
Was bedeutet Prozesse sind langsamer als Threads. genau meinen? Prozesse können nicht langsamer sein als Threads, da in Prozessen kein Code ausgeführt wird. Threads innerhalb dieser Prozesse führen den Code aus. Ich kann nur vermuten, dass das Starten von Prozessen langsamer ist, was richtig ist. Wenn Ihr Programm also häufig neue Threads / Prozesse starten muss, ist die Verwendung multiprocessinglangsamer. In diesem Fall kann jedoch auch Ihre Multithreading-Nutzung erheblich verbessert werden.
Guyarad
103

Sie haben bereits eine ganze Reihe von Antworten erhalten, von "gefälschten Threads" bis hin zu externen Frameworks, aber ich habe niemanden erwähnt Queue.Queue- die "geheime Sauce" des CPython-Threadings.

Zum Erweitern: Solange Sie keine reine Python-CPU-lastige Verarbeitung überlappen müssen (in diesem Fall benötigen Sie dies multiprocessing- aber es kommt auch mit einer eigenen QueueImplementierung, sodass Sie mit einigen erforderlichen Vorsichtsmaßnahmen den allgemeinen Rat I anwenden können Ich gebe ;-), Pythons eingebautes threadingwird es tun ... aber es wird es viel besser machen, wenn Sie es mit Bedacht verwenden , z. B. wie folgt.

Shared Memory "vergessen", angeblich das Hauptvorteil von Threading und Multiprocessing - es funktioniert nicht gut, es skaliert nicht gut, hat es nie, wird es nie. Verwenden Sie den gemeinsam genutzten Speicher nur für Datenstrukturen, die einmal eingerichtet wurden, bevor Sie Sub-Threads erzeugen, und die danach nie mehr geändert wurden. Machen Sie für alles andere einen einzelnen Thread für diese Ressource verantwortlich und kommunizieren Sie mit diesem Thread über Queue.

Weisen Sie jeder Ressource, die Sie normalerweise durch Sperren schützen möchten, einen speziellen Thread zu: eine veränderbare Datenstruktur oder eine zusammenhängende Gruppe davon, eine Verbindung zu einem externen Prozess (einer Datenbank, einem XMLRPC-Server usw.), einer externen Datei usw. Richten Sie einen kleinen Thread-Pool für allgemeine Aufgaben ein, für die keine dedizierte Ressource dieser Art vorhanden ist oder benötigt wird. Erstellen Sie keine Threads nach Bedarf, da Sie sonst durch den Overhead beim Wechseln der Threads überfordert werden.

Die Kommunikation zwischen zwei Threads erfolgt immer über Queue.Queue- eine Form der Nachrichtenübermittlung, die einzige vernünftige Grundlage für die Mehrfachverarbeitung (neben dem vielversprechenden Transaktionsspeicher, für den ich jedoch keine produktionswürdigen Implementierungen außer In Haskell kenne).

Jeder dedizierte Thread, der eine einzelne Ressource (oder einen kleinen zusammenhängenden Satz von Ressourcen) verwaltet, wartet auf Anforderungen in einer bestimmten Queue.Queue-Instanz. Threads in einem Pool warten auf eine einzelne gemeinsam genutzte Queue.Queue (Queue ist solide threadsicher und wird Sie dabei nicht scheitern lassen).

Threads, die nur eine Anforderung in einer Warteschlange (gemeinsam genutzt oder dediziert) in die Warteschlange stellen müssen, tun dies, ohne auf Ergebnisse zu warten, und fahren fort. Threads, die möglicherweise ein Ergebnis oder eine Bestätigung für eine Anforderungswarteschlange benötigen, erhalten ein Paar (Anforderung, Empfangswarteschlange) mit einer Instanz von Queue.Queue, die sie gerade erstellt haben, und schließlich, wenn die Antwort oder Bestätigung für das Fortfahren unabdingbar ist, erhalten sie (Warten) ) aus ihrer Empfangswarteschlange. Stellen Sie sicher, dass Sie bereit sind, sowohl Fehlerantworten als auch echte Antworten oder Bestätigungen zu erhalten (Twisted's deferredsind hervorragend darin, diese Art von strukturierter Antwort zu organisieren, übrigens!).

Sie können Queue auch verwenden, um Instanzen von Ressourcen zu "parken", die von einem beliebigen Thread verwendet werden können, jedoch niemals von mehreren Threads gleichzeitig gemeinsam genutzt werden (DB-Verbindungen mit einigen DBAPI-Komponenten, Cursor mit anderen usw.). Auf diese Weise können Sie sich entspannen Die Anforderung eines dedizierten Threads zugunsten von mehr Pooling (ein Pool-Thread, der aus der gemeinsam genutzten Warteschlange eine Anforderung erhält, die eine in der Warteschlange befindliche Ressource benötigt, erhält diese Ressource aus der entsprechenden Warteschlange und wartet bei Bedarf usw. usw.).

Twisted ist eigentlich eine gute Möglichkeit, dieses Menuett (oder den Square Dance) zu organisieren, nicht nur dank verzögerter, sondern auch aufgrund seiner soliden, soliden, hoch skalierbaren Basisarchitektur: Sie können Dinge so arrangieren, dass Threads oder Unterprozesse nur dann verwendet werden, wenn wirklich gerechtfertigt, während die meisten Dinge, die normalerweise als threadwürdig angesehen werden, in einem einzigen ereignisgesteuerten Thread ausgeführt werden.

Aber mir ist klar, dass Twisted nicht jedermanns Sache ist - der Ansatz "Ressourcen widmen oder bündeln, Warteschlange im Wazoo verwenden, niemals etwas tun, das eine Sperre benötigt, oder, wie Guido verbietet, ein noch weiter fortgeschrittenes Synchronisationsverfahren wie Semaphor oder Bedingung" kann Wird auch dann verwendet, wenn Sie sich nicht mit asynchronen ereignisgesteuerten Methoden auseinandersetzen können, und bietet dennoch mehr Zuverlässigkeit und Leistung als jeder andere weit verbreitete Threading-Ansatz, auf den ich jemals gestoßen bin.

Alex Martelli
quelle
6
Wenn Sie eine Antwort favorisieren könnten, würde ich dies für diese tun. Es ist eines der nachdenklichsten, die ich bei Stack Overflow gefunden habe.
Quark
1
@quark, danke für die freundlichen Worte und froh, dass es dir gefallen hat!
Alex Martelli
4
Ich wünschte, ich könnte dies verdoppeln
Corey Goldberg
2
Dies ist eine starke allgemeine Antwort zum Thema Threading: Der beschriebene Ansatz spiegelt die Auswahl aus anderen Multiprozessorsprachen wider, einschließlich Scala (LinkedBlockingQueue als Hintergrundkonstrukt hinter Actors) und Smalltalk. Eine würdige Lektüre.
StephenBoesch
Toller Beitrag Alex! (nicht wirklich die Frage zu beantworten, aber es lohnt sich zu lesen). Wo der größte Teil des Inhalts unsterblich erscheint (dh auch heute noch relevant ist, obwohl Sie vor etwa 7 Jahren geschrieben haben), habe ich mich gefragt, wie sich Ihre Sichtweise seitdem geändert hat. Fühlen Sie sich frei, auf andere Ressourcen zu verlinken natürlich ...
guyarad
22

Es hängt davon ab, was Sie versuchen, aber ich bin teilweise daran interessiert, nur das threadingModul in der Standardbibliothek zu verwenden, da es wirklich einfach ist, jede Funktion zu übernehmen und sie einfach in einem separaten Thread auszuführen.

from threading import Thread

def f():
    ...

def g(arg1, arg2, arg3=None):
    ....

Thread(target=f).start()
Thread(target=g, args=[5, 6], kwargs={"arg3": 12}).start()

Und so weiter. Ich habe oft ein Producer / Consumer-Setup mit einer synchronisierten Warteschlange, die vom QueueModul bereitgestellt wird

from Queue import Queue
from threading import Thread

q = Queue()
def consumer():
    while True:
        print sum(q.get())

def producer(data_source):
    for line in data_source:
        q.put( map(int, line.split()) )

Thread(target=producer, args=[SOME_INPUT_FILE_OR_SOMETHING]).start()
for i in range(10):
    Thread(target=consumer).start()
Eli Courtwright
quelle
1
Es tut mir leid, wenn ich einen Callable übergeben möchte, der einen Wert als Ziel an einen Thread zurückgibt. Wie kann ich das Ergebnis im Hauptthread abrufen? Ist es möglich oder sollte ich einen Wrapper für die Funktion verwenden, um das Ergebnis in ein modifizierbares Objekt zu setzen? Ich möchte das Ergebnis nicht an das Thread-Objekt selbst binden. Was ist die beste Vorgehensweise? Vielen Dank.
Newtover
3
@newtover: Sie beschreiben immer noch die gleiche grundlegende Threading-Situation zwischen Produzent und Konsument wie in meinem Beispiel. In diesem Fall besteht die Pythonic-Lösung weiterhin darin, ein synchronisiertes Warteschlangenobjekt zu verwenden. Lassen Sie jeden Thread sein Ergebnis in die Warteschlange der Ausgabewerte stellen und vom Hauptthread nach Belieben aus der Warteschlange abrufen. Die Dokumentation für die Queue-Klasse finden Sie unter docs.python.org/library/queue.html. Dort finden Sie sogar ein Beispiel dafür, wie Sie genau das tun, was Sie unter docs.python.org/library/queue.html#Queue.Queue.join
Eli Courtwright
1
Danke für den Link und die Antwort. Noch eine Frage: Gibt es etwas, das einem Wörterbuch mit derselben Funktionalität ähnelt, oder ist es besser, alle Elemente aus der Warteschlange abzurufen und das Wörterbuch selbst zu füllen? Kann ich das integrierte Wörterbuch für diesen Zweck verwenden und die Threads selbst verbinden?
Newtover
2
@newtover: Die Python-Spezifikation garantiert nicht, dass Dikt synchronisiert wird, die CPython-Implementierung jedoch. Wenn Sie also nicht Jython oder PyPy oder IronPython oder etwas anderes verwenden, können Sie möglicherweise ein gewöhnliches Diktat verwenden, je nachdem, was Sie tun. Wenn Sie also nur unterschiedliche Threads haben, die Diktatschlüssel / -werte einstellen, ist das in Ordnung. Wenn Sie jedoch über ein Diktat iterieren oder Diktatwerte lesen / ändern / neu einstellen, müssen Sie wahrscheinlich Ihre eigene Synchronisierung durchführen, wie folgt
Eli Courtwright
1
Meine Aufgabe bestand darin, eine Mapping-Werteberechnung ohne weitere Verarbeitung des Mappings in der Mitte zu parallelisieren. Ich habe das eingebaute Diktat verwendet. Sie bestätigen, dass die CPython-Implementierung von dict synchronisiert ist, daher bleibe ich bei der Lösung. Vielen Dank noch mal.
Newtover
13

Kamaelia ist ein Python-Framework zum Erstellen von Anwendungen mit vielen Kommunikationsprozessen.

(Quelle: kamaelia.org ) Kamaelia - Parallelität nützlich gemacht, Spaß

In Kamaelia erstellen Sie Systeme aus einfachen Komponenten, die miteinander kommunizieren . Dies beschleunigt die Entwicklung, unterstützt die Wartung erheblich und bedeutet auch, dass Sie natürlich gleichzeitig ablaufende Software erstellen . Es soll für jeden Entwickler zugänglich sein , auch für Anfänger. Es macht auch Spaß :)

Welche Art von Systemen? Netzwerkserver, Clients, Desktop-Anwendungen, Pygame-basierte Spiele, Transcode-Systeme und Pipelines, digitale TV-Systeme, Spam-Eradikatoren, Lehrmittel und vieles mehr :)

Hier ist ein Video von Pycon 2009. Zunächst wird Kamaelia mit Twisted und Parallel Python verglichen und anschließend Kamaelia demonstriert.

Einfache Parallelität mit Kamaelia - Teil 1 (59:08)
Einfache Parallelität mit Kamaelia - Teil 2 (18:15)

Sam Hasler
quelle
1
Ich weiß nicht genau, warum jemand diese Antwort notieren würde ... bitte die Abstimmung rückgängig machen ... es sei denn, Sie können einen guten Grund für das
Jon
15
Ich denke, einige Leute hassen Katzen
Sam Hasler
6

In Bezug auf Kamaelia deckt die obige Antwort den Nutzen hier nicht wirklich ab. Kamaelias Ansatz bietet eine einheitliche Schnittstelle, die pragmatisch und nicht perfekt ist, um Threads, Generatoren und Prozesse in einem einzigen System für die Parallelität zu behandeln.

Grundsätzlich bietet es eine Metapher für ein laufendes Objekt mit Posteingängen und Postausgängen. Sie senden Nachrichten an Postausgänge, und wenn sie miteinander verbunden sind, fließen Nachrichten von Postausgängen zu Posteingängen. Diese Metapher / API bleibt gleich, unabhängig davon, ob Sie Generatoren, Threads oder Prozesse verwenden oder mit anderen Systemen sprechen.

Der "nicht perfekte" Teil ist darauf zurückzuführen, dass syntaktischer Zucker für Posteingänge und Postausgänge noch nicht hinzugefügt wurde (obwohl dies diskutiert wird) - es liegt ein Schwerpunkt auf Sicherheit / Benutzerfreundlichkeit im System.

In Kamaelia wird dies anhand des obigen Beispiels für Hersteller und Verbraucher unter Verwendung von Bare-Threading wie folgt:

Pipeline(Producer(), Consumer() )

In diesem Beispiel spielt es keine Rolle, ob es sich um Komponenten mit Gewinde oder auf andere Weise handelt. Der einzige Unterschied besteht aus der Verwendungsperspektive in der Basisklasse für die Komponente. Generatorkomponenten kommunizieren über Listen, Thread-Komponenten über Queue.Queues und prozessbasiert über os.pipes.

Der Grund für diesen Ansatz besteht jedoch darin, das Debuggen von Fehlern zu erschweren. Beim Threading - oder bei jeder Parallelität des gemeinsam genutzten Speichers - besteht das Hauptproblem darin, dass versehentlich Aktualisierungen der gemeinsam genutzten Daten unterbrochen werden. Durch die Verwendung der Nachrichtenübermittlung eliminieren Sie eine Klasse von Fehlern.

Wenn Sie überall nacktes Threading und Sperren verwenden, gehen Sie im Allgemeinen davon aus, dass Sie beim Schreiben von Code keine Fehler machen. Während wir alle danach streben, ist es sehr selten, dass dies passieren wird. Indem Sie das Sperrverhalten an einer Stelle zusammenfassen, vereinfachen Sie, wo etwas schief gehen kann. (Kontexthandler helfen, aber nicht bei versehentlichen Aktualisierungen außerhalb des Kontexthandlers.)

Offensichtlich kann nicht jeder Code als Nachrichtenübermittlung und gemeinsamer Stil geschrieben werden, weshalb Kamaelia auch über einen einfachen Software-Transaktionsspeicher (STM) verfügt, was eine wirklich nette Idee mit einem bösen Namen ist - es ist eher eine Versionskontrolle für Variablen - dh Überprüfen Sie einige Variablen, aktualisieren Sie sie und legen Sie sie fest. Wenn Sie einen Konflikt bekommen, spülen Sie und wiederholen.

Relevante Links:

Wie auch immer, ich hoffe das ist eine nützliche Antwort. FWIW, der Hauptgrund für Kamaelias Setup ist, die Parallelität sicherer und einfacher in Python-Systemen zu verwenden, ohne dass der Schwanz mit dem Hund wedelt. (dh der große Eimer mit Komponenten

Ich kann verstehen, warum die andere Kamaelia-Antwort modifiziert wurde, da sie selbst für mich eher wie eine Anzeige als wie eine Antwort aussieht. Als Autor von Kamaelia ist es schön, Begeisterung zu sehen, obwohl ich hoffe, dass dies etwas relevantere Inhalte enthält :-)

Und das ist meine Art zu sagen, bitte nehmen Sie den Vorbehalt, dass diese Antwort per Definition voreingenommen ist, aber für mich ist es Kamaelias Ziel, zu versuchen, die IMO-Best Practice zu verpacken. Ich würde vorschlagen, ein paar Systeme auszuprobieren und zu sehen, welches für Sie funktioniert. (auch wenn dies für einen Stapelüberlauf ungeeignet ist, sorry - ich bin neu in diesem Forum :-)

Michael Sparks
quelle
3

Ich würde die Microthreads (Tasklets) von Stackless Python verwenden, wenn ich überhaupt Threads verwenden müsste.

Ein ganzes Online-Spiel (Massivly Multiplayer) basiert auf Stackless und seinem Multithreading-Prinzip - da das Original für die Massivly Multiplayer-Eigenschaft des Spiels nur zu langsam ist.

Von Threads in CPython wird dringend abgeraten. Ein Grund ist die GIL - eine globale Interpretersperre -, die das Threading für viele Teile der Ausführung serialisiert. Meine Erfahrung ist, dass es wirklich schwierig ist, auf diese Weise schnelle Anwendungen zu erstellen. Meine Beispielcodierungen waren beim Threading langsamer - mit einem Kern (aber viele Wartezeiten auf die Eingabe hätten einige Leistungssteigerungen ermöglichen sollen).

Verwenden Sie bei CPython nach Möglichkeit lieber separate Prozesse.

Jürgen
quelle
3

Wenn Sie sich wirklich die Hände schmutzig machen möchten, können Sie versuchen, mit Generatoren Coroutinen zu fälschen . Es ist wahrscheinlich nicht das effizienteste in Bezug auf die Arbeit, aber Coroutinen bieten Ihnen eine sehr feine Kontrolle über die Genossenschaft Multitasking und nicht über präventives Multitasking, das Sie anderswo finden.

Ein Vorteil, den Sie feststellen werden, ist, dass Sie im Großen und Ganzen keine Sperren oder Mutexe benötigen, wenn Sie kooperatives Multitasking verwenden. Der wichtigere Vorteil für mich war jedoch die Umschaltgeschwindigkeit zwischen "Threads" von nahezu Null. Natürlich soll Stackless Python auch dafür sehr gut sein; und dann gibt es Erlang, wenn es nicht hat Python sein.

Der wahrscheinlich größte Nachteil beim kooperativen Multitasking ist das generelle Fehlen einer Problemumgehung zum Blockieren von E / A. Und in den gefälschten Coroutinen tritt auch das Problem auf, dass Sie "Threads" nur von der obersten Ebene des Stapels innerhalb eines Threads wechseln können.

Nachdem Sie eine noch etwas komplexere Anwendung mit gefälschten Coroutinen erstellt haben, werden Sie die Arbeit, die mit der Prozessplanung auf Betriebssystemebene verbunden ist, wirklich zu schätzen wissen.

Mark Rushakoff
quelle