J'apprends comment utiliser threading
et les multiprocessing
modules en Python pour exécuter certaines opérations en parallèle et accélérer mon code.
Je trouve cela difficile (peut-être parce que je n'ai aucune base théorique à ce sujet) pour comprendre quelle est la différence entre un threading.Thread()
objet et un objet multiprocessing.Process()
.
De plus, il ne m'est pas tout à fait clair de savoir comment instancier une file d'attente de jobs et n'en avoir que 4 (par exemple) s'exécutant en parallèle, tandis que les autres attendent que les ressources se libèrent avant d'être exécutées.
Je trouve les exemples dans la documentation clairs, mais pas très exhaustifs; dès que j'essaye de compliquer un peu les choses, je reçois beaucoup d'erreurs bizarres (comme une méthode qui ne peut pas être décapée, etc.).
Alors, quand dois-je utiliser les modules threading
et multiprocessing
?
Pouvez-vous me relier à quelques ressources qui expliquent les concepts derrière ces deux modules et comment les utiliser correctement pour des tâches complexes?
Thread
module (appelé_thread
en python 3.x). Pour être honnête, je n'ai jamais compris les différences moi-même ...Thread
/_thread
documentation, ce sont des "primitives de bas niveau". Vous pouvez l'utiliser pour créer des objets de synchronisation personnalisés, pour contrôler l'ordre de jointure d'un arbre de threads, etc. Si vous ne pouvez pas imaginer pourquoi vous devez l'utiliser, ne l'utilisez pas et tenez-vous-enthreading
.Réponses:
Ce que dit Giulio Franco est vrai pour le multithreading par rapport au multiprocessing en général .
Cependant, Python * a un problème supplémentaire: il existe un verrou d'interprétation global qui empêche deux threads du même processus d'exécuter du code Python en même temps. Cela signifie que si vous avez 8 cœurs et que vous modifiez votre code pour utiliser 8 threads, il ne pourra pas utiliser 800% de CPU et fonctionner 8 fois plus vite; il utilisera le même processeur à 100% et fonctionnera à la même vitesse. (En réalité, cela fonctionnera un peu plus lentement, car il y a une surcharge supplémentaire du threading, même si vous n'avez pas de données partagées, mais ignorez cela pour le moment.)
Il y a des exceptions à cela. Si le calcul lourd de votre code ne se produit pas réellement en Python, mais dans une bibliothèque avec du code C personnalisé qui gère correctement GIL, comme une application numpy, vous obtiendrez les performances attendues du threading. La même chose est vraie si le calcul lourd est effectué par un sous-processus que vous exécutez et attendez.
Plus important encore, il y a des cas où cela n'a pas d'importance. Par exemple, un serveur réseau passe la plupart de son temps à lire des paquets hors du réseau, et une application GUI passe le plus clair de son temps à attendre les événements utilisateur. Une des raisons d'utiliser des threads dans un serveur réseau ou une application GUI est de vous permettre d'effectuer des «tâches d'arrière-plan» de longue durée sans empêcher le thread principal de continuer à entretenir des paquets réseau ou des événements GUI. Et cela fonctionne très bien avec les threads Python. (En termes techniques, cela signifie que les threads Python vous offrent la concurrence, même s'ils ne vous donnent pas le parallélisme de cœur.)
Mais si vous écrivez un programme lié au processeur en Python pur, utiliser plus de threads n'est généralement pas utile.
L'utilisation de processus séparés ne pose aucun problème avec le GIL, car chaque processus a son propre GIL distinct. Bien sûr, vous avez toujours les mêmes compromis entre les threads et les processus que dans tous les autres langages - il est plus difficile et plus coûteux de partager des données entre les processus qu'entre les threads, il peut être coûteux d'exécuter un grand nombre de processus ou de créer et de détruire fréquemment, etc. Mais le GIL pèse lourdement sur la balance vers les processus, d'une manière qui n'est pas vraie pour, par exemple, C ou Java. Ainsi, vous vous retrouverez à utiliser le multitraitement beaucoup plus souvent en Python qu'en C ou Java.
Pendant ce temps, la philosophie «batteries incluses» de Python apporte de bonnes nouvelles: il est très facile d'écrire du code qui peut être commuté entre les threads et les processus avec un changement d'une seule ligne.
Si vous concevez votre code en termes de "travaux" autonomes qui ne partagent rien avec d'autres travaux (ou le programme principal) sauf l'entrée et la sortie, vous pouvez utiliser la
concurrent.futures
bibliothèque pour écrire votre code autour d'un pool de threads comme ceci:Vous pouvez même obtenir les résultats de ces travaux et les transmettre à d'autres travaux, attendre les choses dans l'ordre d'exécution ou dans l'ordre d'achèvement, etc. lisez la section sur les
Future
objets pour plus de détails.Maintenant, s'il s'avère que votre programme utilise constamment 100% du processeur et que l'ajout de threads le rend plus lent, alors vous rencontrez le problème GIL, vous devez donc passer aux processus. Tout ce que vous avez à faire est de changer cette première ligne:
La seule vraie mise en garde est que les arguments et les valeurs de retour de vos travaux doivent être picklable (et ne pas prendre trop de temps ou de mémoire à pickler) pour être utilisables cross-process. Ce n'est généralement pas un problème, mais parfois c'est le cas.
Mais que faire si vos emplois ne peuvent pas être autonomes? Si vous pouvez concevoir votre code en termes de tâches qui transmettent des messages de l'un à l'autre, c'est toujours assez facile. Vous devrez peut-être utiliser
threading.Thread
oumultiprocessing.Process
au lieu de compter sur des pools. Et vous devrez créer des objetsqueue.Queue
ou desmultiprocessing.Queue
objets explicitement. (Il existe de nombreuses autres options - tuyaux, sockets, fichiers avec flocks,… mais le fait est que vous devez faire quelque chose manuellement si la magie automatique d'un exécuteur est insuffisante.)Mais que faire si vous ne pouvez même pas compter sur la transmission de messages? Que faire si vous avez besoin de deux emplois pour faire muter la même structure et voir les changements de chacun? Dans ce cas, vous devrez effectuer une synchronisation manuelle (verrous, sémaphores, conditions, etc.) et, si vous souhaitez utiliser des processus, des objets de mémoire partagée explicites pour démarrer. C'est à ce moment que le multithreading (ou multiprocessing) devient difficile. Si vous pouvez l'éviter, tant mieux; si vous ne pouvez pas, vous aurez besoin de lire plus que ce que quelqu'un peut mettre dans une réponse SO.
À partir d'un commentaire, vous vouliez savoir ce qui est différent entre les threads et les processus en Python. Vraiment, si vous lisez la réponse de Giulio Franco et la mienne et tous nos liens, cela devrait tout couvrir ... mais un résumé serait certainement utile, alors voici:
ctypes
types.threading
module ne possède pas certaines des fonctionnalités dumultiprocessing
module. (Vous pouvez utilisermultiprocessing.dummy
pour obtenir la plupart de l'API manquante par-dessus les threads, ou vous pouvez utiliser des modules de niveau supérieur commeconcurrent.futures
et ne pas vous en soucier.)* Ce n'est pas réellement Python, le langage, qui a ce problème, mais CPython, l'implémentation "standard" de ce langage. Certaines autres implémentations n'ont pas de GIL, comme Jython.
** Si vous utilisez la méthode de démarrage de la fourche pour le multitraitement - ce que vous pouvez sur la plupart des plates-formes non Windows - chaque processus enfant obtient toutes les ressources dont le parent disposait au démarrage de l'enfant, ce qui peut être une autre façon de transmettre des données aux enfants.
la source
pickle
documents expliquent), puis au pire votre emballage est stupidedef wrapper(obj, *args): return obj.wrapper(*args)
.Plusieurs threads peuvent exister dans un seul processus. Les threads qui appartiennent au même processus partagent la même zone de mémoire (peuvent lire et écrire dans les mêmes variables, et peuvent interférer les uns avec les autres). Au contraire, différents processus vivent dans des zones de mémoire différentes, et chacun d'eux a ses propres variables. Pour communiquer, les processus doivent utiliser d'autres canaux (fichiers, tubes ou sockets).
Si vous souhaitez paralléliser un calcul, vous aurez probablement besoin du multithreading, car vous souhaitez probablement que les threads coopèrent sur la même mémoire.
En ce qui concerne les performances, les threads sont plus rapides à créer et à gérer que les processus (car le système d'exploitation n'a pas besoin d'allouer une toute nouvelle zone de mémoire virtuelle), et la communication inter-thread est généralement plus rapide que la communication inter-processus. Mais les threads sont plus difficiles à programmer. Les threads peuvent interférer les uns avec les autres et peuvent s'écrire dans la mémoire de l'autre, mais la façon dont cela se produit n'est pas toujours évidente (en raison de plusieurs facteurs, principalement la réorganisation des instructions et la mise en cache de la mémoire), et vous aurez donc besoin de primitives de synchronisation pour contrôler l'accès à vos variables.
la source
threading
etmultiprocessing
.Je pense que ce lien répond à votre question d'une manière élégante.
Pour être bref, si l'un de vos sous-problèmes doit attendre pendant qu'un autre se termine, le multithreading est bon (dans les opérations lourdes d'E / S, par exemple); par contre, si vos sous-problèmes peuvent vraiment survenir en même temps, le multi-traitement est suggéré. Cependant, vous ne créerez pas plus de processus que votre nombre de cœurs.
la source
Citations de la documentation Python
J'ai mis en évidence les citations clés de la documentation Python sur Process vs Threads et le GIL à: Qu'est-ce que le verrou d'interpréteur global (GIL) dans CPython?
Expériences de processus vs threads
J'ai fait un peu de benchmarking afin de montrer la différence plus concrètement.
Dans le benchmark, j'ai chronométré le travail lié au processeur et aux E / S pour différents nombres de threads sur un processeur à 8 hyperthread . Le travail fourni par thread est toujours le même, de sorte que plus de threads signifie plus de travail total fourni.
Les résultats ont été:
Tracer les données .
Conclusions:
pour le travail lié au processeur, le multitraitement est toujours plus rapide, probablement grâce au GIL
pour les travaux liés aux E / S. les deux ont exactement la même vitesse
les threads ne s'adaptent qu'à environ 4x au lieu des 8x attendus puisque je suis sur une machine à 8 hyperthread.
Comparez cela avec un travail lié au processeur C POSIX qui atteint la vitesse de 8x attendue: que signifient «réel», «utilisateur» et «sys» dans la sortie de time (1)?
TODO: Je ne connais pas la raison de cela, il doit y avoir d'autres inefficacités Python en jeu.
Code de test:
GitHub en amont + code de traçage sur le même répertoire .
Testé sur Ubuntu 18.10, Python 3.6.7, dans un ordinateur portable Lenovo ThinkPad P51 avec CPU: CPU Intel Core i7-7820HQ (4 cœurs / 8 threads), RAM: 2x Samsung M471A2K43BB1-CRC (2x 16GiB), SSD: Samsung MZVLB512HAJQ- 000L7 (3 000 Mo / s).
Visualisez quels threads sont en cours d'exécution à un moment donné
Ce post https://rohanvarma.me/GIL/ m'a appris que vous pouvez exécuter un rappel chaque fois qu'un thread est planifié avec l'
target=
argument dethreading.Thread
et de même pourmultiprocessing.Process
.Cela nous permet de voir exactement quel thread s'exécute à chaque fois. Lorsque cela est fait, nous verrons quelque chose comme (j'ai créé ce graphique particulier):
ce qui montrerait que:
la source
Voici quelques données de performances pour python 2.6.x qui remettent en question la notion selon laquelle le threading est plus performant que le multitraitement dans les scénarios liés aux E / S. Ces résultats proviennent d'un IBM System x3650 M4 BD à 40 processeurs.
Traitement lié aux E / S: le pool de processus fonctionne mieux que le pool de threads
Traitement lié au processeur: le pool de processus a mieux fonctionné que le pool de threads
Ce ne sont pas des tests rigoureux, mais ils me disent que le multitraitement n'est pas totalement inefficace par rapport au threading.
Code utilisé dans la console Python interactive pour les tests ci-dessus
la source
>>> do_work(50, 300, 'thread', 'fileio') --> 237.557 ms
>>> do_work(50, 300, 'process', 'fileio') --> 323.963 ms
>>> do_work(50, 2000, 'thread', 'square') --> 232.082 ms
>>> do_work(50, 2000, 'process', 'square') --> 282.785 ms
Eh bien, la plupart de la question est répondue par Giulio Franco. Je vais approfondir le problème du consommateur-producteur, qui, je suppose, vous mettra sur la bonne voie pour votre solution à l'utilisation d'une application multithread.
Vous pouvez en savoir plus sur les primitives de synchronisation à partir de:
Le pseudocode est ci-dessus. Je suppose que vous devriez rechercher le problème producteur-consommateur pour obtenir plus de références.
la source