Threading dans une application PyQt: utiliser des threads Qt ou des threads Python?

117

J'écris une application GUI qui récupère régulièrement des données via une connexion Web. Étant donné que cette récupération prend un certain temps, l'interface utilisateur ne répond pas pendant le processus de récupération (elle ne peut pas être divisée en parties plus petites). C'est pourquoi j'aimerais externaliser la connexion Web vers un thread de travail distinct.

[Oui, je sais, maintenant j'ai deux problèmes .]

Quoi qu'il en soit, l'application utilise PyQt4, alors j'aimerais savoir quel est le meilleur choix: utiliser les threads de Qt ou utiliser le threadingmodule Python ? Quels sont les avantages / inconvénients de chacun? Ou avez-vous une suggestion totalement différente?

Edit (re bounty): Bien que la solution dans mon cas particulier utilisera probablement une demande de réseau non bloquante comme l' ont suggéré Jeff Ober et Lukáš Lalinský (donc en gros, en laissant les problèmes de concurrence à la mise en œuvre du réseau), j'aimerais toujours plus réponse approfondie à la question générale:

Quels sont les avantages et les inconvénients de l'utilisation des threads de PyQt4 (c'est-à-dire de Qt) par rapport aux threads Python natifs (à partir du threadingmodule)?


Edit 2: Merci à tous pour vos réponses. Bien qu'il n'y ait pas d'accord à 100%, il semble y avoir un large consensus sur le fait que la réponse est "utiliser Qt", puisque l'avantage de cela est l'intégration avec le reste de la bibliothèque, sans causer de réels inconvénients.

Pour tous ceux qui cherchent à choisir entre les deux implémentations de threads, je leur recommande vivement de lire toutes les réponses fournies ici, y compris le fil de discussion de la liste de diffusion PyQt vers lequel abbot est lié.

Il y avait plusieurs réponses que j'ai envisagées pour la prime; à la fin j'ai choisi l'abbé comme référence externe très pertinente; c'était, cependant, un appel serré.

Merci encore.

balpha
la source

Réponses:

107

Cela a été discuté il n'y a pas si longtemps dans la liste de diffusion PyQt. Citant les commentaires de Giovanni Bajo sur le sujet:

C'est presque la même chose. La principale différence est que les QThreads sont mieux intégrés à Qt (signaux / slots asynchrones, boucle d'événements, etc.). De plus, vous ne pouvez pas utiliser Qt à partir d'un thread Python (vous ne pouvez pas par exemple publier un événement sur le thread principal via QApplication.postEvent): vous avez besoin d'un QThread pour que cela fonctionne.

Une règle générale pourrait être d'utiliser QThreads si vous allez interagir d'une manière ou d'une autre avec Qt, et d'utiliser les threads Python autrement.

Et un commentaire antérieur sur ce sujet de l'auteur de PyQt: "ils sont tous les deux des wrappers autour des mêmes implémentations de thread natif". Et les deux implémentations utilisent GIL de la même manière.

abbé
la source
2
Bonne réponse, mais je pense que vous devriez utiliser le bouton blockquote pour montrer clairement où vous ne résumez pas, mais citez Giovanni Bajo de la liste de diffusion :)
c089
2
Je me demande pourquoi vous ne pouvez pas publier d'événements sur le thread principal via QApplication.postEvent () et avez besoin d'un QThread pour cela? Je pense avoir vu des gens le faire et cela a fonctionné.
Trilarion
1
J'ai appelé à QCoreApplication.postEventpartir d'un thread Python à une vitesse de 100 fois par seconde, dans une application qui s'exécute sur plusieurs plates-formes et qui a été testée pendant des milliers d'heures. Je n'ai jamais vu de problèmes à ce sujet. Je pense que c'est bien tant que l'objet de destination est situé dans le MainThread ou un QThread. Je l'ai également emballé dans une belle bibliothèque, voir qtutils .
three_pineapples
2
Compte tenu de la nature hautement votée de cette question et réponse, je pense qu'il vaut la peine de souligner une réponse récente de SO par ekhumoro qui explique les conditions dans lesquelles il est sûr d'utiliser certaines méthodes Qt à partir de threads Python. Cela correspond au comportement observé que j'ai vu moi-même et @Trilarion.
three_pineapples
33

Les threads de Python seront plus simples et plus sûrs, et comme il s'agit d'une application basée sur les E / S, ils sont capables de contourner le GIL. Cela dit, avez-vous envisagé des E / S non bloquantes en utilisant des sockets / select torsadés ou non bloquants?

EDIT: plus sur les fils

Fils Python

Les threads de Python sont des threads système. Cependant, Python utilise un verrou d'interpréteur global (GIL) pour s'assurer que l'interpréteur n'exécute qu'un bloc d'une certaine taille d'instructions de code d'octet à la fois. Heureusement, Python publie le GIL pendant les opérations d'entrée / sortie, ce qui rend les threads utiles pour simuler des E / S non bloquantes.

Attention importante: cela peut être trompeur, car le nombre d'instructions d'octet-code ne correspond pas au nombre de lignes dans un programme. Même une seule affectation peut ne pas être atomique en Python, donc un verrou mutex est nécessaire pour tout bloc de code qui doit être exécuté de manière atomique, même avec le GIL.

Fils QT

Lorsque Python transfère le contrôle à un module compilé tiers, il libère le GIL. Il devient de la responsabilité du module d'assurer l'atomicité le cas échéant. Lorsque le contrôle est renvoyé, Python utilisera le GIL. Cela peut rendre l'utilisation de bibliothèques tierces en conjonction avec des threads déroutante. Il est encore plus difficile d'utiliser une bibliothèque de threads externe car cela ajoute une incertitude quant à l'endroit et au moment où le contrôle est entre les mains du module par rapport à l'interpréteur.

Les threads QT fonctionnent avec le GIL publié. Les threads QT sont capables d'exécuter le code de la bibliothèque QT (et tout autre code de module compilé qui n'acquiert pas le GIL) simultanément. Cependant, le code Python exécuté dans le contexte d'un thread QT acquiert toujours le GIL, et vous devez maintenant gérer deux ensembles de logique pour verrouiller votre code.

En fin de compte, les threads QT et les threads Python sont des wrappers autour des threads système. Les threads Python sont légèrement plus sûrs à utiliser, car les parties qui ne sont pas écrites en Python (en utilisant implicitement le GIL) utilisent le GIL dans tous les cas (bien que la mise en garde ci-dessus s'applique toujours.)

E / S non bloquantes

Les threads ajoutent une complexité extraordinaire à votre application. Surtout lorsqu'il s'agit de l'interaction déjà complexe entre l'interpréteur Python et le code du module compilé. Alors que beaucoup trouvent la programmation basée sur les événements difficile à suivre, les E / S basées sur les événements et non bloquantes sont souvent beaucoup moins difficiles à raisonner que les threads.

Avec les E / S asynchrones, vous pouvez toujours être sûr que, pour chaque descripteur ouvert, le chemin d'exécution est cohérent et ordonné. Il y a, évidemment, des problèmes qui doivent être résolus, tels que ce qu'il faut faire lorsque le code dépendant d'un canal ouvert dépend en outre des résultats du code à appeler lorsqu'un autre canal ouvert renvoie des données.

La nouvelle bibliothèque Diesel est une solution intéressante pour les E / S non bloquantes basées sur les événements . Il est limité à Linux pour le moment, mais il est extraordinairement rapide et assez élégant.

Cela vaut également la peine d'apprendre pyevent , un wrapper autour de la merveilleuse bibliothèque libevent, qui fournit un cadre de base pour la programmation événementielle en utilisant la méthode la plus rapide disponible pour votre système (déterminée au moment de la compilation).

Jeff Ober
la source
Re Twisted etc.: J'utilise une bibliothèque tierce qui s'occupe de la mise en réseau; Je voudrais éviter de m'y attacher. Mais je vais toujours examiner cela, merci.
balpha
2
Rien ne contourne réellement le GIL. Mais Python publie le GIL pendant les opérations d'E / S. Python publie également le GIL lors du «transfert» vers des modules compilés, qui sont responsables de l'acquisition / de la publication du GIL eux-mêmes.
Jeff Ober
2
La mise à jour est tout simplement fausse. Le code Python s'exécute exactement de la même manière dans un thread Python que dans un QThread. Vous acquérez le GIL lorsque vous exécutez du code Python (puis Python gère l'exécution entre les threads), vous le libérez lorsque vous exécutez du code C ++. Il n'y a aucune différence.
Lukáš Lalinský
1
Non, le fait est que peu importe la façon dont vous créez le thread, l'interpréteur Python s'en fiche. Tout ce qui compte, c'est qu'il peut acquérir le GIL et après les instructions X, il peut le libérer / le réacquérir. Vous pouvez par exemple utiliser ctypes pour créer un rappel à partir d'une bibliothèque C, qui sera appelée dans un thread séparé, et le code fonctionnera correctement sans même savoir que c'est un thread différent. Il n'y a vraiment rien de spécial dans le module thread.
Lukáš Lalinský
1
Vous disiez en quoi QThread est différent en ce qui concerne le verrouillage et comment "vous devez gérer deux ensembles de logique pour verrouiller votre code". Ce que je dis, c'est que ce n'est pas du tout différent. Je peux utiliser ctypes et pthread_create pour démarrer le fil, et cela fonctionnera exactement de la même manière. Le code Python n'a tout simplement pas à se soucier du GIL.
Lukáš Lalinský
21

L'avantage de QThreadest qu'il est intégré au reste de la bibliothèque Qt. Autrement dit, les méthodes prenant en charge les threads dans Qt devront savoir dans quel thread elles s'exécutent et pour déplacer des objets entre les threads, vous devrez utiliser QThread. Une autre fonctionnalité utile consiste à exécuter votre propre boucle d'événements dans un thread.

Si vous accédez à un serveur HTTP, vous devriez envisager QNetworkAccessManager.

Lukáš Lalinský
la source
1
Mis à part ce que j'ai commenté sur la réponse de Jeff Ober, cela QNetworkAccessManagersemble prometteur. Merci.
balpha
14

Je me suis posé la même question lorsque je travaillais pour PyTalk .

Si vous utilisez Qt, vous devez l'utiliser QThreadpour pouvoir utiliser le framework Qt et en particulier le système signal / slot.

Avec le moteur signal / slot, vous pourrez parler d'un thread à l'autre et avec chaque partie de votre projet.

De plus, il n'y a pas de très question de performance sur ce choix puisque les deux sont des liaisons C ++.

Voici mon expérience de PyQt et du thread.

Je vous encourage à utiliser QThread.

Natim
la source
9

Jeff a quelques bons points. Un seul thread principal peut effectuer des mises à jour de l'interface graphique. Si vous avez besoin de mettre à jour l'interface graphique à partir du thread, les signaux de connexion en file d' attente de Qt-4 facilitent l'envoi de données entre les threads et seront automatiquement appelés si vous utilisez QThread; Je ne sais pas s'ils le seront si vous utilisez des threads Python, bien qu'il soit facile d'ajouter un paramètre à connect().

Kaleb Pederson
la source
5

Je ne peux pas vraiment recommander non plus, mais je peux essayer de décrire les différences entre les threads CPython et Qt.

Tout d'abord, les threads CPython ne s'exécutent pas simultanément, du moins pas le code Python. Oui, ils créent des threads système pour chaque thread Python, mais seul le thread qui détient actuellement Global Interpreter Lock est autorisé à s'exécuter (les extensions C et le code FFI peuvent le contourner, mais le bytecode Python n'est pas exécuté tant que le thread ne contient pas GIL).

D'un autre côté, nous avons des threads Qt, qui sont fondamentalement une couche commune sur les threads système, n'ont pas Global Interpreter Lock, et sont donc capables de fonctionner simultanément. Je ne sais pas comment PyQt le gère, mais à moins que vos threads Qt n'appellent du code Python, ils devraient pouvoir s'exécuter simultanément (à l'exception de divers verrous supplémentaires qui pourraient être implémentés dans diverses structures).

Pour un réglage plus fin, vous pouvez modifier la quantité d'instructions de bytecode qui sont interprétées avant de changer de propriétaire de GIL - des valeurs inférieures signifient plus de changement de contexte (et éventuellement une plus grande réactivité) mais des performances inférieures par thread individuel (les changements de contexte ont leur coût - si vous essayez de changer toutes les quelques instructions, cela n'aide pas à accélérer.)

J'espère que cela vous aidera à résoudre vos problèmes :)

PL
la source
7
Il est important de noter ici: PyQt QThreads prend le verrou d'interprète global . Tout le code Python verrouille le GIL, et tous les QThreads que vous exécutez dans PyQt exécuteront du code Python. (Si ce n'est pas le cas, vous n'utilisez pas réellement la partie "Py" de PyQt :). Si vous choisissez de reporter ce code Python dans une bibliothèque C externe, le GIL sera publié, mais c'est vrai que vous utilisiez un thread Python ou un thread Qt.
quark
C'est en fait ce que j'ai essayé de transmettre, que tout le code Python prend le verrou, mais cela n'a pas d'importance pour le code C / C ++ s'exécutant dans un thread séparé
p_l
0

Je ne peux pas commenter les différences exactes entre les threads Python et PyQt, mais je fais ce que vous essayez de le faire en utilisant QThread, QNetworkAcessManageret en veillant à l' appel QApplication.processEvents()alors que le fil est vivant. Si la réactivité de l'interface graphique est vraiment le problème que vous essayez de résoudre, ce dernier vous aidera.

Brianz
la source
1
QNetworkAcessManagerne nécessite pas de thread ou processEvents. Il utilise des opérations d'E / S asynchrones.
Lukáš Lalinský
Oups ... ouais, j'utilise une combinaison de QNetworkAcessManageret httplib2. Mon code asynchrone utilise httplib2.
brianz