Quelles sont les différences entre les modules threading et multiprocessing?

j'apprends à utiliser les modules threading et multiprocessing 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 pas de formation théorique à ce sujet) de comprendre quelle est la différence entre un objet threading.Thread() et un multiprocessing.Process() .

aussi, il n'est pas tout à fait clair pour moi comment instanciate une file d'emplois et d'avoir seulement 4 (par exemple) d'entre eux courant 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'essaie de compliquer les choses un peu, je reçois beaucoup d'erreurs bizarres (comme une méthode qui ne peut pas être saupoudrée, et ainsi de suite).

alors, Quand dois-je utiliser les modules threading et multiprocessing ?

pouvez-vous me relier à des ressources qui expliquent la les concepts derrière ces deux modules et comment les utiliser correctement pour des tâches complexes?

99
demandé sur coldspeed 2013-08-08 01:37:06

5 réponses

ce que dit Giulio Franco est vrai pour multithreading vs. multiprocessing en général .

Cependant, Python * a une autre question: Il y a un Mondial Interprète de Verrouillage qui empêche deux threads d'un même processus de l'exécution de code Python dans le même temps. Cela signifie que si vous avez 8 cœurs, et changez votre code pour utiliser 8 threads, il ne sera pas en mesure d'utiliser 800% CPU et exécuter 8x plus vite; il utilisera le même CPU à 100% et fonctionnera à la même vitesse. (En réalité, il va fonctionner un peu plus lentement, parce qu'Il ya des frais généraux supplémentaires de threading, même si vous n'avez pas de données partagées, mais ignorez cela pour l'instant.)

il y a des exceptions à cette règle. 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 fait une manipulation correcte de GIL, comme une application numpy, vous obtiendrez l'avantage de performance attendu de threading. La même chose est vrai si le calcul lourd est fait par un sous-processus que vous lancez 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 plus grande partie de son temps à lire des paquets hors du réseau, et une application GUI passe la plus grande partie de son temps à attendre les événements utilisateurs. Une raison d'utiliser des threads dans un serveur réseau ou une application GUI est de vous permettre de faire des "tâches d'arrière-plan" de longue durée sans empêcher le thread principal de continuer à fonctionner. des paquets réseau ou des évènements GUI. Et ça marche très bien avec les fils de Python. (En termes techniques, cela signifie que les threads Python vous donnent la simultanéité, même s'ils ne vous donnent pas le parallélisme de base.)

mais si vous écrivez un programme relié au CPU en Python pur, utiliser plus de threads n'est généralement pas utile.

utilisant des processus séparés n'a pas de tels problèmes avec la GIL, parce que chaque processus a sa propre GIL. Bien sûr, vous avez encore toutes les même compromis entre les threads et les processus que dans n'importe quelle autre langue-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 les détruire fréquemment, etc. Mais la GIL pèse lourdement sur la balance vers les processus, d'une manière qui n'est pas vraie pour, disons, C ou Java. Donc, vous allez vous retrouver à utiliser multiprocessing 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é en va-et-vient entre les fils et les processus avec un changement de couche.

si vous concevez votre code en termes de "jobs" autonomes qui ne partagent rien avec d'autres jobs (ou le programme principal) sauf input ET output, vous pouvez utiliser la bibliothèque concurrent.futures pour écrire votre code autour d'une piscine de fils 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 de les transmettre à d'autres emplois, attendre que les choses dans l'ordre d'exécution ou dans l'ordre 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% CPU, et ajouter plus de threads le rend juste plus lent, alors vous êtes dans le problème GIL, donc vous devez passer à 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 de vos jobs et les valeurs de retour doivent être pickleable (et ne pas prendre trop de temps ou de mémoire à pickle) pour être utilisable Cross-processus. Généralement ce n'est pas un problème, mais parfois, il est.


mais que se passe-t-il si vos emplois ne peuvent pas être autonomes? Si vous pouvez concevoir votre code en termes de travaux que passer messages de l'un à l'autre, c'est encore assez facile. Vous pourriez devoir utiliser threading.Thread ou multiprocessing.Process au lieu de compter sur les piscines. Et vous devrez créer explicitement les objets queue.Queue ou multiprocessing.Queue . (Il y a beaucoup d'autres options-pipes, sockets, fichiers avec flocks, ... mais le point est, 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 message? Et si vous aviez 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 (serrures, sémaphores, conditions, etc.) et, si vous voulez utiliser des processus, des objets de mémoire partagée explicite pour démarrer. C'est quand le multithreading (ou le multiprocessing) devient difficile. Si vous pouvez l'éviter, génial; si vous ne pouvez pas, vous aurez besoin de lire plus que quelqu'un peut mettre dans une réponse SO.


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:

  1. Threads partagent des données par défaut, les processus ne sont pas.
  2. comme conséquence de (1), l'envoi de données entre processus nécessite généralement un décapage et un déballage. **
  3. comme autre conséquence de (1), Le partage direct de données entre les processus nécessite généralement de les mettre dans des formats de bas niveau comme les types Value, Array et ctypes .
  4. Les processus
  5. ne sont pas soumis à la GIL.
  6. sur certaines plateformes (principalement Windows), les processus sont beaucoup plus coûteux à créer et à détruire.
  7. Il y a quelques restrictions supplémentaires sur les processus, certains de qui sont différents sur des plateformes différentes. Voir directives de programmation pour plus de détails.
  8. le module threading ne possède pas certaines des caractéristiques du module multiprocessing . (Vous pouvez utiliser multiprocessing.dummy pour obtenir la plupart des API manquantes sur les threads, ou vous pouvez utiliser des modules de niveau supérieur comme concurrent.futures et ne vous en souciez pas.)

* ce N'est pas vraiment Python., la langue, qui a ce problème, mais Disponible, le "standard" de la mise en œuvre de cette langue. D'autres implémentations n'ont pas de GIL, comme Jython.

* * si vous utilisez la fork start method for multiprocessing-que vous pouvez sur la plupart des plates-formes non Windows-chaque processus enfant reçoit toutes les ressources que le parent a eu lorsque l'enfant a été commencé, ce qui peut être une autre façon de transmettre des données aux enfants.

211
répondu abarnert 2017-05-23 12:26:20

plusieurs threads peuvent exister dans un même processus. Les threads qui appartiennent au même processus partagent la même zone de mémoire (peut lire et écrire sur les mêmes variables, et peuvent interférer les uns avec les autres). Au contraire, différents processus vivent dans différentes zones de mémoire, et chacun d'eux a ses propres variables. Pour communiquer, les processus doivent utiliser d'autres canaux (fichiers, pipes ou sockets).

Si vous voulez paralléliser un calcul, vous êtes probablement besoin de multithreading, parce que vous voulez probablement le fils de coopérer 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 (parce que L'OS n'a pas besoin d'allouer toute 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 fils peuvent interférer les uns avec les autres, et peuvent écrire à la mémoire de l'autre, mais le 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 mémoire), et vous allez donc avoir besoin de primitives de synchronisation pour contrôler l'accès à vos variables.

28
répondu Giulio Franco 2013-08-07 21:53:41

je crois 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 qu'un autre finisse, le multithreading est bon (dans les opérations I/O lourdes, par exemple); en revanche, si vos sous-problèmes peuvent vraiment se produire en même temps, le multiprocessing est suggéré. Cependant, vous ne créerez pas plus de processus que votre nombre de noyaux.

3
répondu ehfaafzv 2017-01-07 04:34:18

voici quelques données de performance pour python 2.6.x qui remet en question la notion que le filetage est plus performant que le multiprocessing dans les scénarios IO-bound. Ces résultats proviennent d'un IBM System x3650 M4 BD à 40 processeurs.

traitement lié à L'entrée : le Pool de procédés est meilleur que le Pool de fils

>>> 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é au processeur: le Pool de processus fonctionne mieux que le Pool de filetage

>>> 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 multiprocessing n'est pas tout à fait non-performant par rapport au threading.

Code utilisé dans la console Python interactive 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')
1
répondu Mario Aguilera 2017-05-31 12:28:12

Eh bien, la plupart de la question Est répondue par Giulio Franco. Je préciserai le consommateur-producteur problème, qui je suppose va vous mettre sur la bonne voie pour votre solution à l'aide d'une application multithread.

fill_count = Semaphore(0) # items produced
empty_count = Semaphore(BUFFER_SIZE) # remaining space
buffer = Buffer()

def producer(fill_count, empty_count, buffer):
    while True:
        item = produceItem()
        empty_count.down();
        buffer.push(item)
        fill_count.up()

def consumer(fill_count, empty_count, buffer):
    while True:
        fill_count.down()
        item = buffer.pop()
        empty_count.up()
        consume_item(item)

vous pouvez en savoir plus sur les primitives de synchronisation de:

 http://linux.die.net/man/7/sem_overview
 http://docs.python.org/2/library/threading.html

Le pseudo-code est ci-dessus. Je suppose que vous devriez chercher le problème producteur-consommateur - pour obtenir plus de références.

-3
répondu innosam 2017-01-07 04:02:18