J'apprends à utiliser les modules threading
et multiprocessing
dans 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 pas de connaissances théoriques à ce sujet) pour comprendre la différence entre un objet threading.Thread()
et un objet multiprocessing.Process()
.
De plus, il ne m'est pas tout à fait clair comment instancier une file d'attente de tâches et n'en avoir que 4 (par exemple) s'exécutant en parallèle, tandis que l'autre attend que les ressources soient libérées avant d'être exécutées.
Je trouve les exemples de la documentation clairs, mais pas très exhaustifs; dès que j'essaie de compliquer un peu les choses, je reçois beaucoup d'erreurs étranges (comme une méthode qui ne peut pas être marinée, etc.).
Alors, quand devrais-je utiliser les modules threading
et multiprocessing
?
Pouvez-vous me relier à des ressources qui expliquent les concepts à la base de ces deux modules et comment les utiliser correctement pour des tâches complexes?
Ce que dit Giulio Franco est vrai pour le multithreading par rapport au multitraitement en général .
Cependant, Python* a un problème supplémentaire: il existe un verrou d'interprète global qui empêche deux threads du même processus d'exécuter le code Python en même temps. Cela signifie que si vous avez 8 cœurs, modifiez votre code en utiliser 8 threads, il ne sera pas capable d'utiliser 800% du processeur et 8x plus vite, il utilisera le même processeur à 100% et s'exécutera à la même vitesse. (En réalité, il fonctionnera un peu plus lentement, car des frais supplémentaires liés aux threads, même si vous n'avez pas de données partagées, mais ignorez-le pour l'instant.)
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 l'avantage 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 attendez.
Plus important encore, il y a des cas où cela n'a pas d'importance. Par exemple, un serveur de réseau passe la majeure partie de son temps à lire des paquets hors réseau et une application à interface graphique passe la plupart de son temps à attendre les événements utilisateur. Une des raisons d'utiliser des threads dans un serveur réseau ou une application d'interface graphique est de vous permettre d'effectuer des "tâches en arrière-plan" de longue durée sans empêcher le thread principal de continuer à traiter les paquets réseau ou les événements d'interface graphique. Et cela fonctionne très bien avec les Python. (En termes techniques, cela signifie Python vous donnent accès aux accès concurrents, même s’ils ne vous donnent pas parallélisme.)
Mais si vous écrivez un programme lié au CPU en Python pur, utiliser plus de threads n'est généralement pas utile.
L'utilisation de processus séparés ne pose pas de tels problèmes avec GIL, car chaque processus a son propre fichier GIL. 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 processus qu'entre threads, il peut être coûteux d'exécuter un grand nombre de processus ou de créer et 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, par exemple, C ou Java. Ainsi, vous utiliserez beaucoup plus souvent le multitraitement en Python qu'en C ou en Java.
En attendant, la philosophie de Python "batteries incluses" apporte quelques bonnes nouvelles: Il est très facile d'écrire du code qui peut être échangé entre les threads et les processus avec un changement direct.
Si vous concevez votre code en termes de "travaux" autonomes qui ne partagent rien avec les autres travaux (ou le programme principal) à l'exception de l'entrée et de la sortie, vous pouvez utiliser le concurrent.futures
librairie 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 tâches et les transmettre à d'autres tâches, attendre que les choses se passent dans l'ordre d'exécution ou d'exécution, etc. lisez la section sur les objets Future
pour plus de détails.
Maintenant, s'il s'avère que votre programme utilise constamment 100% de CPU et que l'ajout de threads le ralentit, 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:
Le seul inconvénient réel est que les arguments et les valeurs de retour de vos travaux doivent être décapables (et ne pas prendre trop de temps ou de mémoire à décaper) pour pouvoir être utilisés en processus croisé. D'habitude ce n'est pas un problème, mais parfois ça l'est.
Mais que se passe-t-il si votre travail ne peut pas être autonome? Si vous pouvez concevoir votre code en termes de travaux qui transmettent des messages de l'un à l'autre, c'est quand même assez simple. Vous devrez peut-être utiliser threading.Thread
ou multiprocessing.Process
au lieu de compter sur des piscines. Et vous devrez créer queue.Queue
ou multiprocessing.Queue
objets explicitement. (Il y a beaucoup d'autres options - pipes, sockets, fichiers avec des flocks,… mais le fait est que vous devez faire quelque chose manuellement si la magie automatique d'un exécuteur est insuffisant.)
Mais que se passe-t-il si vous ne pouvez même pas compter sur la transmission de messages? Que se passe-t-il si vous avez besoin de deux emplois pour 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 à initialiser. C'est à ce moment que le multithreading (ou le multitraitement) devient difficile. Si vous pouvez l'éviter, c'est parfait. si vous ne pouvez pas, vous devrez lire plus que ce que quelqu'un peut mettre dans une réponse SO.
Dans un commentaire, vous vouliez savoir ce qui était 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 couvrir tout… mais un résumé serait certainement utile, alors allez:
ctypes
.threading
ne possède pas certaines des fonctionnalités du module multiprocessing
. (Vous pouvez utiliser multiprocessing.dummy
pour obtenir la plupart des API manquantes au-dessus des threads, ou vous pouvez utiliser des modules de niveau supérieur tels que concurrent.futures
et ne vous inquiétez pas pour ça.)* Ce n'est pas vraiment le langage Python, 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 fork start pour le multitraitement (que vous pouvez utiliser sur la plupart des plates-formes non Windows), chaque processus enfant récupère 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.
Plusieurs threads peuvent exister dans un même processus. Les threads appartenant au même processus partagent la même zone 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, canaux ou sockets).
Si vous souhaitez paralléliser un calcul, vous aurez probablement besoin du multithreading, car vous voulez probablement que les threads coopèrent sur la même mémoire.
En termes de 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 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 écrire dans la mémoire de chacun, mais la façon dont cela se produit n'est pas toujours évidente (en raison de plusieurs facteurs, principalement le réordonnancement des instructions et la mise en cache de la mémoire). Vous aurez donc besoin de primitives de synchronisation pour contrôler l'accès. à vos variables.
Je crois que ce lien répond à votre question de manière élégante.
Pour résumer, si l’un de vos sous-problèmes doit attendre pendant que l’autre se termine, le multithreading est bon (pour les opérations lourdes d’E/S, par exemple); au contraire, si vos sous-problèmes peuvent vraiment se produire en même temps, le multitraitement est suggéré. Cependant, vous ne créerez pas plus de processus que votre nombre de cœurs.
Voici quelques données de performance pour python 2.6.x qui appelle à remettre en question la notion selon laquelle le threading est plus performant que le multitraitement dans des scénarios liés à l'IO. Ces résultats proviennent d'un serveur IBM System x3650 M4 à 40 processeurs. .
Traitement lié à l'IO: le pool de processus a mieux performé que le pool de threads
>>> do_work(50, 300, 'thread','fileio')
do_work function took 455.752 ms
>>> do_work(50, 300, 'process','fileio')
do_work function took 319.279 ms
Traitement lié à la CPU: le pool de processus a mieux performé que le pool de threads
>>> do_work(50, 2000, 'thread','square')
do_work function took 338.309 ms
>>> do_work(50, 2000, 'process','square')
do_work function took 287.488 ms
Ce ne sont pas des tests rigoureux, mais ils me disent que le multitraitement n'est pas tout à fait non performant par rapport au threading.
Code utilisé dans la console interactive python pour les tests ci-dessus)
from multiprocessing import Pool
from multiprocessing.pool import ThreadPool
import time
import sys
import os
from glob import glob
text_for_test = str(range(1,100000))
def fileio(i):
try :
os.remove(glob('./test/test-*'))
except :
pass
f=open('./test/test-'+str(i),'a')
f.write(text_for_test)
f.close()
f=open('./test/test-'+str(i),'r')
text = f.read()
f.close()
def square(i):
return i*i
def timing(f):
def wrap(*args):
time1 = time.time()
ret = f(*args)
time2 = time.time()
print '%s function took %0.3f ms' % (f.func_name, (time2-time1)*1000.0)
return ret
return wrap
result = None
@timing
def do_work(process_count, items, process_type, method) :
pool = None
if process_type == 'process' :
pool = Pool(processes=process_count)
else :
pool = ThreadPool(processes=process_count)
if method == 'square' :
multiple_results = [pool.apply_async(square,(a,)) for a in range(1,items)]
result = [res.get() for res in multiple_results]
else :
multiple_results = [pool.apply_async(fileio,(a,)) for a in range(1,items)]
result = [res.get() for res in multiple_results]
do_work(50, 300, 'thread','fileio')
do_work(50, 300, 'process','fileio')
do_work(50, 2000, 'thread','square')
do_work(50, 2000, 'process','square')