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:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
executor.submit(job, argument)
executor.map(some_function, collection_of_independent_things)
# ...
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:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
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
ou multiprocessing.Process
au lieu de compter sur des pools. Et vous devrez créer des objets queue.Queue
ou des multiprocessing.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:
- Les threads partagent des données par défaut; les processus ne le font pas.
- En conséquence de (1), l'envoi de données entre les processus nécessite généralement un décapage et un décapage. **
- Comme autre conséquence de (1), le partage direct des données entre les processus nécessite généralement de les mettre dans des formats de bas niveau tels que Value, Array et
ctypes
types.
- Les processus ne sont pas soumis au GIL.
- Sur certaines plates-formes (principalement Windows), les processus sont beaucoup plus coûteux à créer et à détruire.
- Il existe des restrictions supplémentaires sur les processus, dont certaines sont différentes selon les plates-formes. Voir les directives de programmation pour plus de détails.
- Le
threading
module ne possède pas certaines des fonctionnalités du multiprocessing
module. (Vous pouvez utiliser multiprocessing.dummy
pour obtenir la plupart de l'API manquante par-dessus les threads, ou vous pouvez utiliser des modules de niveau supérieur comme concurrent.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.
Thread
module (appelé_thread
en python 3.x). Pour être honnête, je n'ai jamais compris les différences moi-même ...