Threading in un'applicazione PyQt: utilizzare thread Qt o thread Python?


116

Sto scrivendo un'applicazione GUI che recupera regolarmente i dati tramite una connessione web. Poiché questo recupero richiede un po 'di tempo, l'interfaccia utente non risponde durante il processo di recupero (non può essere suddivisa in parti più piccole). Questo è il motivo per cui vorrei esternalizzare la connessione Web a un thread di lavoro separato.

[Sì, lo so, ora ho due problemi .]

Ad ogni modo, l'applicazione utilizza PyQt4, quindi mi piacerebbe sapere qual è la scelta migliore: utilizzare i thread di Qt o utilizzare il threadingmodulo Python ? Quali sono i vantaggi / svantaggi di ciascuno? O hai un suggerimento completamente diverso?

Modifica (re bounty): mentre la soluzione nel mio caso particolare probabilmente utilizzerà una richiesta di rete non bloccante come suggerita da Jeff Ober e Lukáš Lalinský (quindi sostanzialmente lasciando i problemi di concorrenza all'implementazione della rete), mi piacerebbe ancora risposta approfondita alla domanda generale:

Quali sono i vantaggi e gli svantaggi dell'utilizzo dei thread di PyQt4 (cioè Qt) rispetto ai thread Python nativi (dal threadingmodulo)?


Modifica 2: grazie a tutti per le vostre risposte. Sebbene non ci sia un accordo al 100%, sembra esserci un consenso diffuso sul fatto che la risposta sia "usa Qt", poiché il vantaggio di ciò è l'integrazione con il resto della libreria, senza causare svantaggi reali.

A chiunque cerchi di scegliere tra le due implementazioni di threading, consiglio vivamente di leggere tutte le risposte fornite qui, incluso il thread della mailing list PyQt a cui si collega l' abate .

C'erano diverse risposte che ho preso in considerazione per la taglia; alla fine ho scelto l'abate per il riferimento esterno molto rilevante; era, tuttavia, una chiamata ravvicinata.

Grazie ancora.

Risposte:


106

Questo è stato discusso non molto tempo fa nella mailing list di PyQt. Citando i commenti di Giovanni Bajo sull'argomento:

È per lo più lo stesso. La differenza principale è che i QThread sono meglio integrati con Qt (segnali / slot asincroni, loop di eventi, ecc.). Inoltre, non puoi usare Qt da un thread Python (non puoi ad esempio postare un evento sul thread principale tramite QApplication.postEvent): hai bisogno di un QThread perché funzioni.

Una regola generale potrebbe essere quella di utilizzare QThreads se hai intenzione di interagire in qualche modo con Qt e altrimenti utilizzare i thread Python.

E qualche commento precedente su questo argomento dall'autore di PyQt: "sono entrambi wrapper attorno alle stesse implementazioni di thread nativi". Ed entrambe le implementazioni usano GIL allo stesso modo.


2
Buona risposta, ma penso che dovresti usare il pulsante blockquote per mostrare chiaramente dove stai in effetti non riassumendo ma citando Giovanni Bajo dalla mailing list :)
c089

2
Mi chiedo perché non puoi inviare eventi al thread principale tramite QApplication.postEvent () e hai bisogno di un QThread per questo? Penso di aver visto persone farlo e ha funzionato.
Trilarion

1
Ho chiamato QCoreApplication.postEventda un thread Python a una velocità di 100 volte al secondo, in un'applicazione che funziona su più piattaforme ed è stata testata per migliaia di ore. Non ho mai visto problemi da questo. Penso che vada bene finché l'oggetto di destinazione si trova in MainThread o in QThread. L'ho anche racchiuso in una bella libreria, vedi qtutils .
three_pineapples

2
Data la natura altamente votata di questa domanda e risposta, penso che valga la pena sottolineare una recente risposta SO di ekhumoro che elabora le condizioni in cui è sicuro utilizzare determinati metodi Qt dai thread Python. Questo corrisponde al comportamento osservato che è stato visto da me e da @Trilarion.
three_pineapples

33

I thread di Python saranno più semplici e sicuri e, poiché è per un'applicazione basata su I / O, sono in grado di bypassare il GIL. Detto questo, hai considerato l'I / O non bloccante utilizzando prese / selezione Twisted o non bloccanti?

EDIT: altro sui thread

Thread di Python

I thread di Python sono thread di sistema. Tuttavia, Python utilizza un blocco dell'interprete globale (GIL) per garantire che l'interprete esegua sempre e solo un blocco di determinate dimensioni di istruzioni byte-code alla volta. Fortunatamente, Python rilascia il GIL durante le operazioni di input / output, rendendo i thread utili per la simulazione dell'I / O non bloccante.

Avvertenza importante: questo può essere fuorviante, poiché il numero di istruzioni byte-code non corrisponde al numero di righe in un programma. Anche una singola assegnazione potrebbe non essere atomica in Python, quindi è necessario un blocco mutex per qualsiasi blocco di codice che deve essere eseguito atomicamente, anche con GIL.

Discussioni QT

Quando Python cede il controllo a un modulo compilato di terze parti, rilascia il GIL. Diventa responsabilità del modulo garantire l'atomicità dove richiesto. Quando il controllo viene restituito, Python utilizzerà il GIL. Ciò può creare confusione nell'utilizzo di librerie di terze parti insieme a thread. È ancora più difficile utilizzare una libreria di threading esterna perché aggiunge incertezza su dove e quando il controllo è nelle mani del modulo rispetto all'interprete.

I thread QT funzionano con il GIL rilasciato. I thread QT sono in grado di eseguire il codice della libreria QT (e altro codice del modulo compilato che non acquisisce il GIL) contemporaneamente. Tuttavia, il codice Python eseguito nel contesto di un thread QT acquisisce ancora il GIL e ora devi gestire due set di logica per bloccare il tuo codice.

Alla fine, sia i thread QT che i thread Python sono wrapper attorno ai thread di sistema. I thread Python sono leggermente più sicuri da usare, poiché quelle parti che non sono scritte in Python (implicitamente usando il GIL) usano il GIL in ogni caso (anche se l'avvertenza sopra si applica ancora).

I / O non bloccante

I thread aggiungono una complessità straordinaria alla tua applicazione. Soprattutto quando si ha a che fare con la già complessa interazione tra l'interprete Python e il codice del modulo compilato. Mentre molti trovano difficile seguire la programmazione basata su eventi, l'I / O basato su eventi e non bloccante è spesso molto meno difficile da ragionare rispetto ai thread.

Con l'I / O asincrono, puoi sempre essere sicuro che, per ogni descrittore aperto, il percorso di esecuzione sia coerente e ordinato. Ci sono, ovviamente, problemi che devono essere affrontati, come cosa fare quando il codice che dipende da un canale aperto dipende ulteriormente dai risultati del codice da chiamare quando un altro canale aperto restituisce dati.

Una buona soluzione per l'I / O basato su eventi e non bloccante è la nuova libreria Diesel . Al momento è limitato a Linux, ma è straordinariamente veloce e piuttosto elegante.

Vale anche la pena dedicare del tempo a imparare pyevent , un wrapper attorno alla meravigliosa libreria libevent, che fornisce un framework di base per la programmazione basata su eventi utilizzando il metodo più veloce disponibile per il tuo sistema (determinato in fase di compilazione).


Re Twisted ecc .: Uso una libreria di terze parti che fa le vere cose di rete; Vorrei evitare di rattopparlo. Ma lo esaminerò ancora, grazie.
balpha

2
Niente in realtà aggira il GIL. Ma Python rilascia il GIL durante le operazioni di I / O. Python rilascia anche il GIL quando "passa" ai moduli compilati, che sono responsabili dell'acquisizione / rilascio del GIL stesso.
Jeff Ober,

2
L'aggiornamento è semplicemente sbagliato. Il codice Python viene eseguito esattamente allo stesso modo in un thread Python che in un QThread. Acquisisci il GIL quando esegui codice Python (e quindi Python gestisce l'esecuzione tra i thread), lo rilasci quando esegui codice C ++. Non c'è nessuna differenza.
Lukáš Lalinský

1
No, il punto è che non importa come crei il thread, all'interprete Python non importa. Tutto ciò che gli importa è che possa acquisire il GIL e dopo le istruzioni di X lo possa rilasciare / riacquistare. Ad esempio, puoi utilizzare ctypes per creare un callback da una libreria C, che verrà chiamata in un thread separato e il codice funzionerà correttamente senza nemmeno sapere che è un thread diverso. Non c'è davvero niente di speciale nel modulo thread.
Lukáš Lalinský,

1
Stavi dicendo come QThread è diverso per quanto riguarda il blocco e come "devi gestire due set di logica per bloccare il tuo codice". Quello che sto dicendo è che non è affatto diverso. Posso usare ctypes e pthread_create per avviare il thread e funzionerà esattamente allo stesso modo. Il codice Python semplicemente non deve preoccuparsi del GIL.
Lukáš Lalinský

21

Il vantaggio di QThreadè che è integrato con il resto della libreria Qt. Cioè, i metodi thread-aware in Qt dovranno sapere in quale thread vengono eseguiti e per spostare gli oggetti tra i thread, sarà necessario utilizzare QThread. Un'altra caratteristica utile è eseguire il proprio ciclo di eventi in un thread.

Se stai accedendo a un server HTTP, dovresti considerare QNetworkAccessManager.


1
A parte quello che ho commentato sulla risposta di Jeff Ober, QNetworkAccessManagersembra promettente. Grazie.
balpha

13

Mi sono posto la stessa domanda mentre lavoravo su PyTalk .

Se stai usando Qt, devi usare QThreadper poter usare il framework Qt e in particolare il sistema segnale / slot.

Con il signal / slot engine potrai parlare da un thread all'altro e con ogni parte del tuo progetto.

Inoltre, non c'è molta domanda sulle prestazioni di questa scelta poiché entrambi sono collegamenti C ++.

Ecco la mia esperienza di PyQt e thread.

Ti incoraggio a usare QThread.


9

Jeff ha dei buoni punti. Solo un thread principale può eseguire aggiornamenti della GUI. Se è necessario aggiornare la GUI dall'interno del thread, i segnali di connessione in coda di Qt-4 semplificano l'invio dei dati attraverso i thread e verranno richiamati automaticamente se si utilizza QThread; Non sono sicuro se lo saranno se stai usando thread Python, anche se è facile aggiungere un parametro a connect().


5

Non posso davvero consigliarlo, ma posso provare a descrivere le differenze tra i thread CPython e Qt.

Prima di tutto, i thread CPython non vengono eseguiti contemporaneamente, almeno non il codice Python. Sì, creano thread di sistema per ogni thread Python, tuttavia può essere eseguito solo il thread che attualmente detiene Global Interpreter Lock (le estensioni C e il codice FFI potrebbero bypassarlo, ma il bytecode Python non viene eseguito mentre il thread non contiene GIL).

D'altra parte, abbiamo i thread Qt, che sono fondamentalmente uno strato comune sui thread di sistema, non hanno Global Interpreter Lock e quindi sono in grado di funzionare contemporaneamente. Non sono sicuro di come PyQt lo gestisca, tuttavia, a meno che i tuoi thread Qt non chiamino codice Python, dovrebbero essere in grado di funzionare contemporaneamente (a parte vari blocchi extra che potrebbero essere implementati in varie strutture).

Per un'ulteriore messa a punto, puoi modificare la quantità di istruzioni bytecode che vengono interpretate prima di cambiare proprietà di GIL: valori più bassi significano più cambio di contesto (e possibilmente una maggiore reattività) ma prestazioni inferiori per singolo thread (i cambi di contesto hanno il loro costo - se tu prova a cambiare ogni poche istruzioni che non aiuta ad accelerare.)

Spero che ti aiuti con i tuoi problemi :)


7
È importante notare qui: PyQt QThreads accetta il Global Interpreter Lock . Tutto il codice Python blocca il GIL e qualsiasi QThread eseguito in PyQt eseguirà codice Python. (Se non lo fanno, in realtà non stai utilizzando la parte "Py" di PyQt :). Se scegli di rimandare da quel codice Python a una libreria C esterna, il GIL verrà rilasciato, ma ciò è vero indipendentemente dal fatto che utilizzi un thread Python o un thread Qt.
quark

Questo era in realtà quello che ho cercato di trasmettere, che tutto il codice Python prende il blocco, ma non importa per il codice C / C ++ in esecuzione in thread separati
p_l

0

Non posso commentare sulle differenze esatte tra le discussioni Python e PyQt, ma ho fatto quello che stai cercando di fare uso QThread, QNetworkAcessManagere fare in modo di chiamata QApplication.processEvents()mentre il thread è vivo. Se la reattività della GUI è davvero il problema che stai cercando di risolvere, il secondo ti aiuterà.


1
QNetworkAcessManagernon richiede un thread o processEvents. Utilizza operazioni di I / O asincrone.
Lukáš Lalinský,

Oops ... sì, sto usando una combinazione di QNetworkAcessManagere httplib2. Il mio codice asincrono utilizza httplib2.
brianz
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.