213 votes

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

J'apprends à utiliser le threading et le multiprocessing en Python pour exécuter certaines opérations en parallèle et accélérer mon code.

J'ai du mal (peut-être parce que je n'ai aucun bagage théorique à ce sujet) à comprendre quelle est la différence entre une threading.Thread() et un objet multiprocessing.Process() un.

De même, je ne vois pas très bien comment instancier une file d'attente de travaux et faire en sorte que seuls 4 (par exemple) d'entre eux s'exécutent en parallèle, tandis que les autres attendent que les ressources se libèrent avant d'être exécutés.

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 bizarres (comme une méthode qui ne peut pas être décapée, etc.).

Alors, quand dois-je utiliser le threading y multiprocessing des modules ?

Pouvez-vous m'indiquer des ressources qui expliquent les concepts derrière ces deux modules et comment les utiliser correctement pour des tâches complexes ?

348voto

abarnert Points 94246

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 :

  1. Les threads partagent les données par défaut ; les processus ne le font pas.
  2. En conséquence de (1), l'envoi de données entre processus nécessite généralement un décapage et un dépiquage. **
  3. 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.
  4. Les processus ne sont pas soumis au GIL.
  5. Sur certaines plateformes (principalement Windows), les processus sont beaucoup plus coûteux à créer et à détruire.
  6. 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.
  7. 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.

51voto

Giulio Franco Points 1269

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

Si vous voulez 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 nouvelle zone de mémoire virtuelle), et la communication entre les threads est généralement plus rapide que la communication entre les processus. Mais les threads sont plus difficiles à programmer. Les threads peuvent interférer les uns avec les autres, et peuvent écrire dans la mémoire des autres, 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), et vous aurez donc besoin de primitives de synchronisation pour contrôler l'accès à vos variables.

15voto

Ciro Santilli Points 3341

Citations de la documentation Python

J'ai mis en évidence les principales citations de la documentation Python concernant les processus et les threads ainsi que la GIL : Qu'est-ce que le verrou global de l'interpréteur (GIL) dans CPython ?

Expériences sur les processus et les fils

J'ai fait un peu de benchmarking afin de montrer la différence de manière plus concrète.

Dans le benchmark, j'ai chronométré le travail lié au CPU et aux entrées/sorties pour différents nombres de threads sur un ordinateur de type 8 hyperthreads CPU. 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 sont les suivants :

enter image description here

Données de traçage .

Conclusions :

  • pour les travaux liés à l'unité centrale, le multitraitement est toujours plus rapide, vraisemblablement en raison de la GIL

  • pour les travaux liés aux IO. Les deux sont exactement à la même vitesse.

  • Les threads ne s'étendent que jusqu'à environ 4x au lieu des 8x attendus puisque je suis sur une machine à 8 hyperthreads.

    Comparez cela avec un travail C POSIX lié au CPU qui atteint la vitesse attendue de 8x : Que signifient 'real', 'user' et 'sys' dans la sortie de time(1) ?

    TODO : Je ne connais pas la raison de ce phénomène, il doit y avoir d'autres inefficacités de Python qui entrent en jeu.

Code de test :

#!/usr/bin/env python3

import multiprocessing
import threading
import time
import sys

def cpu_func(result, niters):
    '''
    A useless CPU bound function.
    '''
    for i in range(niters):
        result = (result * result * i + 2 * result * i * i + 3) % 10000000
    return result

class CpuThread(threading.Thread):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class CpuProcess(multiprocessing.Process):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class IoThread(threading.Thread):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

class IoProcess(multiprocessing.Process):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

if __name__ == '__main__':
    cpu_n_iters = int(sys.argv[1])
    sleep = 1
    cpu_count = multiprocessing.cpu_count()
    input_params = [
        (CpuThread, cpu_n_iters),
        (CpuProcess, cpu_n_iters),
        (IoThread, sleep),
        (IoProcess, sleep),
    ]
    header = ['nthreads']
    for thread_class, _ in input_params:
        header.append(thread_class.__name__)
    print(' '.join(header))
    for nthreads in range(1, 2 * cpu_count):
        results = [nthreads]
        for thread_class, work_size in input_params:
            start_time = time.time()
            threads = []
            for i in range(nthreads):
                thread = thread_class(work_size)
                threads.append(thread)
                thread.start()
            for i, thread in enumerate(threads):
                thread.join()
            results.append(time.time() - start_time)
        print(' '.join('{:.6e}'.format(result) for result in results))

GitHub upstream + traçage du code sur le même répertoire .

Testé sur Ubuntu 18.10, Python 3.6.7, dans un ordinateur portable Lenovo ThinkPad P51 avec CPU : Intel Core i7-7820HQ CPU (4 cores / 8 threads), RAM : 2x Samsung M471A2K43BB1-CRC (2x 16GiB), SSD : Samsung MZVLB512HAJQ-000L7 (3 000 Mo/s).

Visualiser les threads en cours d'exécution à un moment donné

Ce poste https://rohanvarma.me/GIL/ m'a appris que l'on peut exécuter un callback à chaque fois qu'un thread est planifié avec la fonction target= argument de threading.Thread et de même pour multiprocessing.Process .

Cela nous permet de voir exactement quel thread fonctionne à chaque instant. Lorsque cela est fait, nous verrons quelque chose comme (j'ai inventé ce graphique particulier) :

            +--------------------------------------+
            + Active threads / processes           +
+-----------+--------------------------------------+
|Thread   1 |********     ************             |
|         2 |        *****            *************|
+-----------+--------------------------------------+
|Process  1 |***  ************** ******  ****      |
|         2 |** **** ****** ** ********* **********|
+-----------+--------------------------------------+
            + Time -->                             +
            +--------------------------------------+

ce qui montrerait que :

  • les threads sont entièrement sérialisés par le GIL
  • les processus peuvent fonctionner en parallèle

5voto

ehfaafzv Points 171

Je crois ce lien répond à votre question de manière élégante.

En bref, si l'un de vos sous-problèmes doit attendre qu'un autre se termine, le multithreading est bon (dans les opérations lourdes d'E/S, par exemple) ; en revanche, si vos sous-problèmes peuvent vraiment se dérouler en même temps, le multiprocessing est suggéré. Cependant, vous ne créerez pas plus de processus que votre nombre de cœurs.

2voto

Mario Aguilera Points 408

Voici quelques données de performance pour python 2.6.x qui remettent en question l'idée que le threading est plus performant que le multiprocessing dans les scénarios liés aux entrées-sorties. Ces résultats proviennent d'un IBM System x3650 M4 BD à 40 processeurs.

Traitement lié aux entrées-sorties : le pool de processus est plus performant 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é au CPU : le pool de processus est plus performant 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 m'indiquent que le multitraitement n'est pas totalement inefficace 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')

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X