Threading in einer PyQt-Anwendung: Verwenden Sie Qt-Threads oder Python-Threads?

116

Ich schreibe eine GUI-Anwendung, die regelmäßig Daten über eine Webverbindung abruft. Da dieser Abruf eine Weile dauert, reagiert die Benutzeroberfläche während des Abrufvorgangs nicht mehr (sie kann nicht in kleinere Teile aufgeteilt werden). Aus diesem Grund möchte ich die Webverbindung an einen separaten Arbeitsthread auslagern.

[Ja, ich weiß, jetzt habe ich zwei Probleme .]

Wie auch immer, die Anwendung verwendet PyQt4, daher möchte ich wissen, was die bessere Wahl ist: Verwenden Sie die Threads von Qt oder das Python- threadingModul? Was sind die Vor- und Nachteile der einzelnen? Oder haben Sie einen ganz anderen Vorschlag?

Bearbeiten (erneutes Kopfgeld): Während die Lösung in meinem speziellen Fall wahrscheinlich darin besteht, eine nicht blockierende Netzwerkanforderung zu verwenden, wie von Jeff Ober und Lukáš Lalinský vorgeschlagen (so dass die Parallelitätsprobleme im Grunde der Netzwerkimplementierung überlassen bleiben ), möchte ich dennoch eine weitere ausführliche Antwort auf die allgemeine Frage:

Welche Vor- und Nachteile hat die Verwendung von PyQt4-Threads (dh Qt-Threads) gegenüber nativen Python-Threads (aus dem threadingModul)?


Edit 2: Vielen Dank für Ihre Antworten. Obwohl es keine 100% ige Übereinstimmung gibt, scheint es einen weit verbreiteten Konsens darüber zu geben, dass die Antwort "Qt verwenden" lautet, da der Vorteil darin besteht, dass die Integration in den Rest der Bibliothek erfolgt und keine wirklichen Nachteile entstehen.

Für alle, die zwischen den beiden Threading-Implementierungen wählen möchten, empfehle ich dringend, alle hier bereitgestellten Antworten zu lesen, einschließlich des PyQt-Mailinglisten-Threads, auf den der Abt verweist.

Es gab mehrere Antworten, die ich für das Kopfgeld in Betracht gezogen hatte; Am Ende wählte ich Abt als sehr relevante externe Referenz; Es war jedoch ein enger Anruf.

Danke noch einmal.

balpha
quelle

Antworten:

106

Dies wurde vor nicht allzu langer Zeit in der PyQt-Mailingliste besprochen . Zitat von Giovanni Bajos Kommentaren zu diesem Thema:

Es ist meistens das gleiche. Der Hauptunterschied besteht darin, dass QThreads besser in Qt integriert sind (asynchrone Signale / Slots, Ereignisschleife usw.). Sie können Qt auch nicht aus einem Python-Thread verwenden (Sie können beispielsweise kein Ereignis über QApplication.postEvent an den Hauptthread senden): Sie benötigen einen QThread, damit dies funktioniert.

Eine allgemeine Faustregel könnte sein, QThreads zu verwenden, wenn Sie irgendwie mit Qt interagieren möchten, und ansonsten Python-Threads zu verwenden.

Und einige frühere Kommentare zu diesem Thema von PyQts Autor: "Beide sind Wrapper für dieselben nativen Thread-Implementierungen." Und beide Implementierungen verwenden GIL auf die gleiche Weise.

Abt
quelle
2
Gute Antwort, aber ich denke, Sie sollten die Blockquote-Schaltfläche verwenden, um klar zu zeigen, wo Sie tatsächlich nicht zusammenfassen, sondern Giovanni Bajo aus der Mailingliste zitieren :)
c089
2
Ich frage mich, warum Sie keine Ereignisse über QApplication.postEvent () im Hauptthread veröffentlichen können und dafür einen QThread benötigen. Ich glaube, ich habe Leute gesehen, die das gemacht haben, und es hat funktioniert.
Trilarion
1
Ich habe QCoreApplication.postEventvon einem Python-Thread mit einer Geschwindigkeit von 100 Mal pro Sekunde in einer Anwendung aufgerufen , die plattformübergreifend ausgeführt wird und seit 1000 Stunden getestet wurde. Ich habe noch nie Probleme damit gesehen. Ich denke, es ist in Ordnung, solange sich das Zielobjekt im MainThread oder einem QThread befindet. Ich habe es auch in eine schöne Bibliothek eingepackt , siehe qtutils .
Drei_Pineapples
2
Angesichts der hohen Stimmenzahl dieser Frage und Antwort lohnt es sich, auf eine aktuelle SO-Antwort von ekhumoro hinzuweisen, in der die Bedingungen erläutert werden, unter denen bestimmte Qt-Methoden aus Python-Threads sicher verwendet werden können. Dies stimmt mit dem beobachteten Verhalten überein, das ich und @Trilarion gesehen haben.
Drei_Pineapples
33

Pythons Threads werden einfacher und sicherer, und da es sich um eine E / A-basierte Anwendung handelt, können sie die GIL umgehen. Haben Sie in Betracht gezogen, nicht blockierende E / A mit Twisted- oder nicht blockierenden Sockets / Select zu blockieren?

EDIT: mehr zu Threads

Python-Threads

Pythons Threads sind System-Threads. Python verwendet jedoch eine globale Interpretersperre (GIL), um sicherzustellen, dass der Interpreter immer nur einen Block mit Bytecode-Anweisungen einer bestimmten Größe gleichzeitig ausführt. Glücklicherweise gibt Python die GIL während der Eingabe- / Ausgabeoperationen frei, wodurch Threads für die Simulation nicht blockierender E / A nützlich sind.

Wichtiger Hinweis: Dies kann irreführend sein, da die Anzahl der Bytecode-Anweisungen nicht der Anzahl der Zeilen in einem Programm entspricht. Selbst eine einzelne Zuweisung ist in Python möglicherweise nicht atomar, daher ist eine Mutex-Sperre für jeden Codeblock erforderlich , der auch mit der GIL atomar ausgeführt werden muss.

QT-Threads

Wenn Python die Kontrolle an ein kompiliertes Modul eines Drittanbieters übergibt, gibt es die GIL frei. Es liegt in der Verantwortung des Moduls, bei Bedarf die Atomizität sicherzustellen. Wenn die Kontrolle zurückgegeben wird, verwendet Python die GIL. Dies kann die Verwendung von Bibliotheken von Drittanbietern in Verbindung mit Threads verwirrend machen. Es ist noch schwieriger, eine externe Threading-Bibliothek zu verwenden, da dadurch die Unsicherheit darüber erhöht wird, wo und wann die Steuerung in den Händen des Moduls gegenüber dem Interpreter liegt.

QT-Threads arbeiten mit freigegebener GIL. QT-Threads können gleichzeitig QT-Bibliothekscode (und anderen kompilierten Modulcode, der die GIL nicht erfasst) ausführen. Der im Kontext eines QT-Threads ausgeführte Python-Code erhält jedoch weiterhin die GIL, und jetzt müssen Sie zwei Logiksätze zum Sperren Ihres Codes verwalten.

Am Ende sind sowohl QT-Threads als auch Python-Threads Wrapper um System-Threads. Python-Threads sind geringfügig sicherer zu verwenden, da diejenigen Teile, die nicht in Python geschrieben sind (implizit die GIL verwenden), auf jeden Fall die GIL verwenden (obwohl der obige Vorbehalt weiterhin gilt).

Nicht blockierende E / A.

Threads machen Ihre Anwendung außerordentlich komplex. Besonders wenn es um die bereits komplexe Interaktion zwischen dem Python-Interpreter und dem kompilierten Modulcode geht. Während es für viele schwierig ist, ereignisbasierte Programmierung zu befolgen, ist es oft weniger schwierig, über ereignisbasierte, nicht blockierende E / A nachzudenken als über Threads.

Mit asynchroner E / A können Sie immer sicher sein, dass der Ausführungspfad für jeden offenen Deskriptor konsistent und geordnet ist. Es gibt offensichtlich Probleme, die behoben werden müssen, z. B. was zu tun ist, wenn Code in Abhängigkeit von einem offenen Kanal weiter von den Ergebnissen des Codes abhängt, der aufgerufen werden soll, wenn ein anderer offener Kanal Daten zurückgibt.

Eine gute Lösung für ereignisbasierte, nicht blockierende E / A ist die neue Diesel- Bibliothek. Es ist momentan auf Linux beschränkt, aber es ist außerordentlich schnell und recht elegant.

Es lohnt sich auch, pyevent zu lernen , einen Wrapper um die wunderbare Libevent-Bibliothek, der ein grundlegendes Framework für die ereignisbasierte Programmierung mit der schnellsten verfügbaren Methode für Ihr System bietet (festgelegt zur Kompilierungszeit).

Jeff Ober
quelle
Re Twisted etc.: Ich verwende eine Bibliothek eines Drittanbieters, die die eigentlichen Netzwerkaufgaben erledigt. Ich möchte vermeiden, darin herumzupicken. Aber ich werde mich trotzdem darum kümmern, danke.
Balpha
2
Nichts umgeht tatsächlich die GIL. Python gibt die GIL jedoch während der E / A-Operationen frei. Python gibt die GIL auch frei, wenn es an kompilierte Module übergeben wird, die für die Erfassung / Freigabe der GIL selbst verantwortlich sind.
Jeff Ober
2
Das Update ist einfach falsch. Python-Code wird in einem Python-Thread genauso ausgeführt wie in einem QThread. Sie geben die GIL frei, wenn Sie Python-Code ausführen (und dann verwaltet Python die Ausführung zwischen Threads). Sie geben sie frei, wenn Sie C ++ - Code ausführen. Es gibt überhaupt keinen Unterschied.
Lukáš Lalinský
1
Nein, der Punkt ist, dass es dem Python-Interpreter egal ist, wie Sie den Thread erstellen. Alles, was es interessiert, ist, dass es die GIL erwerben und nach X-Anweisungen freigeben / wieder erwerben kann. Sie können beispielsweise ctypes verwenden, um einen Rückruf aus einer C-Bibliothek zu erstellen, der in einem separaten Thread aufgerufen wird, und der Code funktioniert einwandfrei, ohne zu wissen, dass es sich um einen anderen Thread handelt. Das Thread-Modul hat wirklich nichts Besonderes.
Lukáš Lalinský
1
Sie sagten, wie sich QThread beim Sperren unterscheidet und wie "Sie zwei Logiksätze zum Sperren Ihres Codes verwalten müssen". Was ich sage ist, dass es überhaupt nicht anders ist. Ich kann ctypes und pthread_create verwenden, um den Thread zu starten, und es wird genauso funktionieren. Python-Code muss sich einfach nicht um die GIL kümmern.
Lukáš Lalinský
21

Der Vorteil von QThreadist, dass es in den Rest der Qt-Bibliothek integriert ist. Das heißt, thread-fähige Methoden in Qt müssen wissen, in welchem ​​Thread sie ausgeführt werden, und um Objekte zwischen Threads zu verschieben, müssen Sie sie verwenden QThread. Eine weitere nützliche Funktion ist das Ausführen einer eigenen Ereignisschleife in einem Thread.

Wenn Sie auf einen HTTP-Server zugreifen, sollten Sie dies berücksichtigen QNetworkAccessManager.

Lukáš Lalinský
quelle
1
Abgesehen von dem, was ich zu Jeff Obers Antwort kommentiert habe, QNetworkAccessManagersieht es vielversprechend aus. Vielen Dank.
Balpha
13

Ich habe mir dieselbe Frage gestellt, als ich bei PyTalk gearbeitet habe .

Wenn Sie Qt verwenden, müssen Sie verwenden QThread, um das Qt-Framework und insbesondere das Signal- / Slot-System verwenden zu können.

Mit der Signal- / Slot-Engine können Sie von einem Thread zu einem anderen und mit jedem Teil Ihres Projekts sprechen.

Darüber hinaus gibt es keine sehr leistungsbedingte Frage zu dieser Auswahl, da beide C ++ - Bindungen sind.

Hier ist meine Erfahrung mit PyQt und Thread.

Ich ermutige Sie zu verwenden QThread.

Natim
quelle
9

Jeff hat einige gute Punkte. Nur ein Hauptthread kann GUI-Updates durchführen. Wenn Sie die GUI innerhalb des Threads aktualisieren müssen, erleichtern die Verbindungssignale in der Warteschlange von Qt-4 das Senden von Daten über Threads und werden automatisch aufgerufen, wenn Sie QThread verwenden. Ich bin mir nicht sicher, ob dies der Fall sein wird, wenn Sie Python-Threads verwenden, obwohl es einfach ist, einen Parameter hinzuzufügen connect().

Kaleb Pederson
quelle
5

Ich kann es auch nicht wirklich empfehlen, aber ich kann versuchen, Unterschiede zwischen CPython- und Qt-Threads zu beschreiben.

Erstens werden CPython-Threads nicht gleichzeitig ausgeführt, zumindest nicht Python-Code. Ja, sie erstellen Systemthreads für jeden Python-Thread, es darf jedoch nur der Thread ausgeführt werden, der derzeit Global Interpreter Lock enthält (C-Erweiterungen und FFI-Code können dies umgehen, aber Python-Bytecode wird nicht ausgeführt, während der Thread keine GIL enthält).

Auf der anderen Seite haben wir Qt-Threads, die im Grunde eine gemeinsame Schicht über System-Threads sind, keine globale Interpreter-Sperre haben und daher gleichzeitig ausgeführt werden können. Ich bin mir nicht sicher, wie PyQt damit umgeht. Wenn Ihre Qt-Threads jedoch keinen Python-Code aufrufen, sollten sie gleichzeitig ausgeführt werden können (abgesehen von verschiedenen zusätzlichen Sperren, die möglicherweise in verschiedenen Strukturen implementiert sind).

Für eine zusätzliche Feinabstimmung können Sie die Anzahl der Bytecode-Anweisungen ändern, die vor dem Eigentümerwechsel von GIL interpretiert werden. Niedrigere Werte bedeuten mehr Kontextwechsel (und möglicherweise eine höhere Reaktionsfähigkeit), aber eine geringere Leistung pro einzelnem Thread (Kontextwechsel haben ihre Kosten - wenn Sie dies tun versuche alle paar Anweisungen zu wechseln, es hilft nicht bei der Geschwindigkeit.)

Hoffe es hilft bei deinen Problemen :)

p_l
quelle
7
Hierbei ist zu beachten: PyQt QThreads verwenden die globale Interpreter-Sperre . Der gesamte Python-Code sperrt die GIL, und alle QThreads, die Sie in PyQt ausführen, führen Python-Code aus. (Wenn nicht, verwenden Sie nicht den "Py" -Teil von PyQt :). Wenn Sie diesen Python-Code in eine externe C-Bibliothek verschieben, wird die GIL freigegeben. Dies gilt jedoch unabhängig davon, ob Sie einen Python-Thread oder einen Qt-Thread verwenden.
Quark
Das war eigentlich das, was ich zu vermitteln versuchte, dass der gesamte Python-Code die Sperre übernimmt, aber es spielt keine Rolle, ob C / C ++ - Code in einem separaten Thread ausgeführt wird
p_l
0

Ich kann die genauen Unterschiede zwischen Python- und PyQt-Threads nicht kommentieren, aber ich habe das getan, was Sie versuchen QThread, QNetworkAcessManagerund sichergestellt, dass Sie aufrufen, QApplication.processEvents()während der Thread aktiv ist. Wenn die Reaktionsfähigkeit der Benutzeroberfläche wirklich das Problem ist, das Sie lösen möchten, hilft das letztere.

brianz
quelle
1
QNetworkAcessManagerbenötigt keinen Thread oder processEvents. Es werden asynchrone E / A-Operationen verwendet.
Lukáš Lalinský
Ups ... ja, ich benutze eine Kombination aus QNetworkAcessManagerund httplib2. Mein asynchroner Code verwendet httplib2.
Brianz