Ce que dit Giulio Franco est vrai pour le multithreading et le multiprocessing. en général .
Cependant, Python * a un problème supplémentaire : Il existe un verrouillage global de l'interpréteur 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 qu'il utilise 8 threads, il ne pourra pas utiliser 800% du CPU et fonctionner 8 fois plus vite ; il utilisera les mêmes 100% du CPU et fonctionnera à la même vitesse. (En réalité, il s'exécutera un peu plus lentement, car il y a une surcharge supplémentaire due au threading, même si vous n'avez pas de données partagées, mais ignorez cela pour l'instant).
Il existe des exceptions à cette règle. Si les calculs lourds de votre code ne sont pas effectués dans Python, mais dans une bibliothèque avec du code C personnalisé qui gère correctement la GIL, comme une application numpy, vous obtiendrez le gain de performance attendu du threading. Il en va de même si le calcul lourd est effectué par un sous-processus que vous exécutez et sur lequel vous attendez.
Plus important encore, il existe des cas où cela n'a pas d'importance. Par exemple, un serveur réseau passe la plupart de son temps à lire des paquets sur le réseau, et une application GUI passe la plupart de son temps à attendre les événements de l'utilisateur. L'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 à traiter les paquets réseau ou les é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 offrent pas le parallélisme des noyaux).
Mais si vous écrivez un programme lié au processeur en Python pur, l'utilisation de plus de threads n'est généralement pas utile.
L'utilisation de processus distincts ne pose pas de tels problèmes avec le GIL, car chaque processus possède son propre GIL distinct. Bien sûr, vous avez toujours les mêmes compromis entre les threads et les processus que dans n'importe quel autre langage - 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 les créer et de les détruire fréquemment, etc. Mais le GIL pèse lourdement sur la balance en faveur des processus, d'une manière qui n'est pas vraie pour, disons, C ou Java. Ainsi, vous vous retrouverez à utiliser le multiprocessing beaucoup plus souvent en Python qu'en C ou en Java.
En attendant, la philosophie "batteries incluses" de Python apporte une bonne nouvelle : il est très facile d'écrire du code qui peut passer d'un thread à l'autre et d'un processus à l'autre en modifiant une seule ligne.
Si vous concevez votre code en termes de "tâches" autonomes qui ne partagent rien avec d'autres tâches (ou le programme principal) à l'exception de l'entrée et de la sortie, vous pouvez utiliser la fonction concurrent.futures
pour écrire votre code autour d'un pool de threads comme celui-ci :
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 des choses dans l'ordre d'exécution ou dans l'ordre d'achèvement, etc. Future
pour plus de détails.
Maintenant, s'il s'avère que votre programme utilise constamment 100 % du CPU et que l'ajout de threads ne fait que le ralentir, alors vous rencontrez le problème de la GIL et vous devez passer aux processus. Tout ce que vous avez à faire est de changer la première ligne :
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
Le seul véritable problème est que les arguments et les valeurs de retour de vos tâches doivent pouvoir être récupérés (et ne pas prendre trop de temps ou de mémoire pour être récupérés) pour être utilisables en inter-processus. En général, ce n'est pas un problème, mais c'est parfois 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 messages passagers de l'un à l'autre, c'est quand même assez facile. Vous devrez peut-être utiliser threading.Thread
ou multiprocessing.Process
au lieu de s'appuyer sur des pools. Et vous devrez créer queue.Queue
ou multiprocessing.Queue
de manière explicite. (Il existe de nombreuses autres options : les tuyaux, les sockets, les fichiers avec les flocks, etc. quelque chose manuellement si la magie automatique d'un exécuteur est insuffisante).
Mais que se passe-t-il si vous ne pouvez même pas compter sur le passage des messages ? Que faire si vous avez besoin que deux tâches mutent la même structure, et voient les changements de l'autre ? Dans ce cas, vous devrez procéder à une synchronisation manuelle (verrous, sémaphores, conditions, etc.) et, si vous souhaitez utiliser des processus, des objets à mémoire partagée explicites pour démarrer. C'est là que le multithreading (ou multiprocessing) devient difficile. Si vous pouvez l'éviter, tant mieux ; si vous ne le pouvez pas, vous devrez lire plus que ce que quelqu'un peut mettre dans une réponse SO.
Dans un commentaire, vous vouliez savoir quelle est la différence entre les threads et les processus en Python. En fait, si vous lisez la réponse de Giulio Franco, la mienne et tous nos liens, cela devrait tout couvrir mais un résumé serait certainement utile, donc voici :
- Les threads partagent les données par défaut ; les processus ne le font pas.
- En conséquence de (1), l'envoi de données entre processus nécessite généralement un décapage et un dépiquage. **
- Autre conséquence de (1), le partage direct de données entre processus nécessite généralement de les placer dans des formats de bas niveau comme Value, Array et
ctypes
types.
- Les processus ne sont pas soumis au GIL.
- Sur certaines plateformes (principalement Windows), les processus sont beaucoup plus coûteux à créer et à détruire.
- Il existe quelques restrictions supplémentaires sur les processus, dont certaines sont différentes selon les plateformes. Voir Directives de programmation pour les détails.
- El
threading
ne dispose pas de certaines des fonctionnalités du module multiprocessing
module. (Vous pouvez utiliser multiprocessing.dummy
pour obtenir la plupart des API manquantes au dessus des threads, ou vous pouvez utiliser des modules de plus haut niveau comme concurrent.futures
et ne pas s'en inquiéter).
* Ce n'est pas vraiment 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 le <a href="https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods" rel="noreferrer">fourchette </a>pour le multitraitement - ce qui est possible sur la plupart des plates-formes non-Windows - chaque processus enfant obtient toutes les ressources dont disposait le parent au moment du démarrage de l'enfant, ce qui peut être un autre moyen de transmettre des données aux enfants.