Pourquoi, quand et comment utiliser le multithreading et le multiprocessing en Python

Salut, Khabrovsk. En ce moment, OTUS a ouvert un ensemble pour le cours de Machine Learning , à cet égard, nous avons traduit pour vous un «conte de fées» trÚs intéressant. Aller.




Il Ă©tait une fois, dans une galaxie lointaine, lointaine ... Un

sorcier sage et puissant vivait dans un petit village au milieu du désert. Et leur nom était Dumbledalf. Non seulement il était sage et puissant, mais il a également aidé des gens venus de pays lointains à demander l'aide d'un sorcier. Notre histoire a commencé lorsqu'un voyageur a apporté au sorcier un parchemin magique. Le voyageur ne savait pas ce qu'il y avait dans le parchemin, il savait seulement que si quelqu'un pouvait révéler tous les secrets du parchemin, alors c'était Dumbledalf.

Chapitre 1: Single-Thread, Single-Process


Si vous n'avez pas encore deviné, j'ai fait une analogie avec le processeur et ses fonctions. Notre assistant est un processeur, et un parchemin est une liste de liens qui mÚnent à la puissance et aux connaissances de Python pour le maßtriser.

La premiÚre pensée d'un sorcier qui a déchiffré la liste sans aucune difficulté a été d'envoyer son fidÚle ami (Garrigorn? Je sais, je sais que cela semble horrible) à chacun des endroits décrits dans le parchemin afin de trouver et d'apporter ce qu'il pouvait y trouver.

In [1]:
import urllib.request
from concurrent.futures import ThreadPoolExecutor
In [2]:
urls = [
  'http://www.python.org',
  'https://docs.python.org/3/',
  'https://docs.python.org/3/whatsnew/3.7.html',
  'https://docs.python.org/3/tutorial/index.html',
  'https://docs.python.org/3/library/index.html',
  'https://docs.python.org/3/reference/index.html',
  'https://docs.python.org/3/using/index.html',
  'https://docs.python.org/3/howto/index.html',
  'https://docs.python.org/3/installing/index.html',
  'https://docs.python.org/3/distributing/index.html',
  'https://docs.python.org/3/extending/index.html',
  'https://docs.python.org/3/c-api/index.html',
  'https://docs.python.org/3/faq/index.html'
  ]
In [3]:
%%time

results = []
for url in urls:
    with urllib.request.urlopen(url) as src:
        results.append(src)

CPU times: user 135 ms, sys: 283 ”s, total: 135 ms
Wall time: 12.3 s
In [ ]:

Comme vous pouvez le voir, nous parcourons simplement les URL une par une en utilisant la boucle for et lisons la réponse. Grùce à %% time et à la magie d' IPython , nous pouvons voir que cela a pris environ 12 secondes avec mon triste Internet.

Chapitre 2: Multithreading


Ce n'Ă©tait pas sans raison que le sorcier Ă©tait cĂ©lĂšbre pour sa sagesse; il a rapidement rĂ©ussi Ă  trouver un moyen beaucoup plus efficace. Au lieu d'envoyer une personne Ă  chaque endroit dans l'ordre, pourquoi ne pas rĂ©unir une Ă©quipe d'associĂ©s fiables et les envoyer dans diffĂ©rentes parties du monde en mĂȘme temps! L'assistant pourra unir toutes les connaissances qu'il apporte en mĂȘme temps!

C'est vrai, au lieu d'afficher la liste dans une boucle en séquence, nous pouvons utiliser le multithreading pour accéder à plusieurs URL à la fois.

In [1]:
import urllib.request
from concurrent.futures import ThreadPoolExecutor
In [2]:
urls = [
  'http://www.python.org',
  'https://docs.python.org/3/',
  'https://docs.python.org/3/whatsnew/3.7.html',
  'https://docs.python.org/3/tutorial/index.html',
  'https://docs.python.org/3/library/index.html',
  'https://docs.python.org/3/reference/index.html',
  'https://docs.python.org/3/using/index.html',
  'https://docs.python.org/3/howto/index.html',
  'https://docs.python.org/3/installing/index.html',
  'https://docs.python.org/3/distributing/index.html',
  'https://docs.python.org/3/extending/index.html',
  'https://docs.python.org/3/c-api/index.html',
  'https://docs.python.org/3/faq/index.html'
  ]
In [4]:
%%time

with ThreadPoolExecutor(4) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 122 ms, sys: 8.27 ms, total: 130 ms
Wall time: 3.83 s
In [5]:
%%time

with ThreadPoolExecutor(8) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 122 ms, sys: 14.7 ms, total: 137 ms
Wall time: 1.79 s
In [6]:
%%time

with ThreadPoolExecutor(16) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 143 ms, sys: 3.88 ms, total: 147 ms
Wall time: 1.32 s
In [ ]:

Bien mieux! Presque comme ... de la magie. L'utilisation de plusieurs threads peut accĂ©lĂ©rer considĂ©rablement de nombreuses tĂąches d'E / S. Dans mon cas, la plupart du temps passĂ© Ă  lire les URL est dĂ» Ă  la latence du rĂ©seau. Les programmes liĂ©s aux E / S passent la majeure partie de leur vie Ă  attendre, vous l'aurez devinĂ©, une entrĂ©e ou une sortie (tout comme un sorcier attend que ses amis se rendent Ă  des endroits du dĂ©filement et reviennent). Cela peut ĂȘtre entrĂ© / sorti Ă  partir du rĂ©seau, de la base de donnĂ©es, du fichier ou de l'utilisateur. Ces E / S prennent gĂ©nĂ©ralement beaucoup de temps, car la source peut avoir besoin d'effectuer un prĂ©traitement avant de transfĂ©rer les donnĂ©es vers les E / S. Par exemple, un processeur comptera beaucoup plus rapidement qu'une connexion rĂ©seau transmettra des donnĂ©es (Ă  une vitesse d'environcomme Flash contre votre grand-mĂšre).

Remarque : multithreadingpeut ĂȘtre trĂšs utile dans des tĂąches telles que le nettoyage de pages Web.

Chapitre 3: Multiprocessing


Les annĂ©es ont passĂ©, la renommĂ©e du bon sorcier a grandi, et avec elle a grandi l'envie d'un sorcier noir impartial (Sarumort? Ou peut-ĂȘtre Volandeman?). ArmĂ© d'une ruse incommensurable et poussĂ© par l'envie, le sorcier noir jeta une terrible malĂ©diction sur Dumbledalf. Lorsque la malĂ©diction le rattrapa, Dumbledalf se rendit compte qu'il n'avait que quelques instants pour le repousser. DĂ©sespĂ©rĂ©, il fouilla dans ses livres de sorts et trouva rapidement un contre-sort qui Ă©tait censĂ© fonctionner. Le seul problĂšme Ă©tait que l'assistant devait calculer la somme de tous les nombres premiers infĂ©rieurs Ă  1 000 000. Un sort Ă©trange, bien sĂ»r, mais ce que nous avons.

Le sorcier savait que calculer la valeur serait trivial s'il avait assez de temps, mais il n'avait pas ce luxe. MalgrĂ© le fait qu'il soit un grand sorcier, il est limitĂ© par son humanitĂ© et ne peut vĂ©rifier la simplicitĂ© qu'un seul numĂ©ro Ă  la fois. S'il dĂ©cidait de simplement additionner les nombres premiers les uns aprĂšs les autres, cela prendrait trop de temps. Lorsqu'il ne restait que quelques secondes avant d'appliquer le contre-sort , il se souvint soudain du sort de multi- traitement , qu'il avait appris du parchemin magique il y a de nombreuses annĂ©es. Ce sort lui permettra de se copier afin de rĂ©partir les nombres entre ses copies et d'en vĂ©rifier plusieurs en mĂȘme temps. Et Ă  la fin, tout ce qu'il doit faire, c'est simplement additionner les chiffres que lui et ses copies dĂ©couvriront.

In [1]:
from multiprocessing import Pool
In [2]:
def if_prime(x):
    if x <= 1:
        return 0
    elif x <= 3:
        return x
    elif x % 2 == 0 or x % 3 == 0:
        return 0
    i = 5
    while i**2 <= x:
        if x % i == 0 or x % (i + 2) == 0:
            return 0
        i += 6
    return x
In [17]:
%%time

answer = 0

for i in range(1000000):
    answer += if_prime(i)

CPU times: user 3.48 s, sys: 0 ns, total: 3.48 s
Wall time: 3.48 s
In [18]:
%%time

if __name__ == '__main__':
    with Pool(2) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

CPU times: user 114 ms, sys: 4.07 ms, total: 118 ms
Wall time: 1.91 s
In [19]:
%%time

if __name__ == '__main__':
    with Pool(4) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

CPU times: user 99.5 ms, sys: 30.5 ms, total: 130 ms
Wall time: 1.12 s
In [20]:
%%timeit

if __name__ == '__main__':
    with Pool(8) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

729 ms ± 3.02 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [21]:
%%timeit

if __name__ == '__main__':
    with Pool(16) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

512 ms ± 39.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [22]:
%%timeit

if __name__ == '__main__':
    with Pool(32) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

518 ms ± 13.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [23]:
%%timeit

if __name__ == '__main__':
    with Pool(64) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

621 ms ± 10.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [ ]:

Les processeurs modernes ont plus d'un cƓur, nous pouvons donc accĂ©lĂ©rer les tĂąches en utilisant le multiprocessing du module de traitement multiprocess . Les tĂąches associĂ©es au processeur sont des programmes qui effectuent la plupart du temps des calculs sur le processeur (calculs mathĂ©matiques triviaux, traitement d'image, etc.). Si les calculs peuvent ĂȘtre effectuĂ©s indĂ©pendamment les uns des autres, nous avons la possibilitĂ© de les diviser entre les cƓurs de processeur disponibles, obtenant ainsi une augmentation significative de la vitesse de traitement.

Tout ce que tu dois faire est:

  1. DĂ©terminez la fonction Ă  appliquer.
  2. Préparer une liste d'éléments auxquels la fonction sera appliquée;
  3. multiprocessing.Pool. , Pool(), . with , .
  4. Pool map. map , , .

Remarque : Une fonction peut ĂȘtre dĂ©finie pour effectuer n'importe quelle tĂąche pouvant ĂȘtre effectuĂ©e en parallĂšle. Par exemple, une fonction peut contenir du code pour Ă©crire le rĂ©sultat d'un calcul dans un fichier.

Alors, pourquoi devons-nous séparermultiprocessingetmultithreading? Si vous avez déjà essayé d'améliorer les performances d'une tùche sur le processeur en utilisant le multithreading, le résultat est exactement le contraire. C'est tout simplement terrible! Voyons comment cela s'est produit.

Tout comme un assistant est limité par sa nature humaine et ne peut calculer qu'un seul nombre par unité de temps, Python est livré avec une chose appelée Global Interpreter Lock (GIL) . Python est heureux de vous permettre de générer autant de threads que vous le souhaitez, mais GILgarantit qu'un seul de ces threads s'exécutera à un moment donné.

Pour une tĂąche liĂ©e aux E / S, cette situation est tout Ă  fait normale. Un thread envoie une demande Ă  une URL et attend une rĂ©ponse, alors ce thread peut ĂȘtre remplacĂ© par un autre, qui enverra une autre demande Ă  une URL diffĂ©rente. Étant donnĂ© que le thread n'a rien Ă  faire jusqu'Ă  ce qu'il reçoive une rĂ©ponse, cela ne fait aucune diffĂ©rence qu'Ă  un moment donnĂ©, un seul thread est en cours d'exĂ©cution.

Pour les tĂąches effectuĂ©es sur le processeur, avoir plusieurs threads est presque aussi inutile que les mamelons dans l'armure. Étant donnĂ© qu'un seul thread peut ĂȘtre exĂ©cutĂ© par unitĂ© de temps, mĂȘme si vous en gĂ©nĂ©rez plusieurs, chacun se verra attribuer un numĂ©ro pour vĂ©rifier la simplicitĂ©, le processeur fonctionnera toujours avec un seul thread. En fait, ces chiffres seront toujours vĂ©rifiĂ©s un par un. Et la surcharge de travail avec plusieurs threads contribuera Ă  rĂ©duire les performances, ce que vous pouvez simplement observer lorsqu'il est utilisĂ© multithreadingdans des tĂąches effectuĂ©es sur le processeur.

Pour contourner cette «restriction», nous utilisons le modulemultiprocessing. Au lieu d'utiliser des threads, le multitraitement utilise, comme vous le dites ... plusieurs processus. Chaque processus a son propre interprĂšte et son propre espace mĂ©moire, donc le GIL ne vous limitera pas. En fait, chaque processus utilisera son propre cƓur de processeur et fonctionnera avec son propre numĂ©ro unique, et cela sera effectuĂ© simultanĂ©ment avec le travail d'autres processus. Comme c'est gentil de leur part!

Vous remarquerez peut-ĂȘtre que la charge du processeur sera plus Ă©levĂ©e lorsque vous l'utiliserez multiprocessingpar rapport Ă  une boucle for rĂ©guliĂšre ou mĂȘme multithreading. Cela se produit car votre programme utilise non pas un cƓur, mais plusieurs. Et c'est bien!

rappelez-vous, quemultiprocessingIl a ses propres frais généraux de gestion de plusieurs processus, ce qui est généralement plus grave que le coût du multithreading. (Le multitraitement génÚre des interprÚtes séparés et attribue sa propre zone de mémoire à chaque processus, alors oui!) C'est-à-dire, en rÚgle générale, il est préférable d'utiliser une version allégée du multithreading lorsque vous souhaitez sortir de cette maniÚre (rappelez-vous des tùches liées aux E / S). Mais lorsque le calcul sur le processeur devient un goulot d'étranglement, l'heure du module arrive multiprocessing. Mais souvenez-vous qu'avec une grande puissance, il y a une grande responsabilité.

Si vous gĂ©nĂ©rez plus de processus que votre processeur ne peut en traiter par unitĂ© de temps, vous remarquerez que les performances commenceront Ă  dĂ©cliner. Cela se produit car le systĂšme d'exploitation a besoin de faire plus de travail en mĂ©langeant les processus entre les cƓurs de processeur, car il y a plus de processus. En rĂ©alitĂ©, tout peut ĂȘtre encore plus compliquĂ© que je ne vous l'ai dit aujourd'hui, mais j'ai vĂ©hiculĂ© l'idĂ©e principale. Par exemple, sur mon systĂšme, les performances chutent lorsque le nombre de processus est de 16. C'est parce qu'il n'y a que 16 cƓurs logiques dans mon processeur.

Chapitre 4: Conclusion


  • Dans les tĂąches liĂ©es aux E / S, cela multithreadingpeut amĂ©liorer les performances.
  • Dans les tĂąches liĂ©es aux E / S, multiprocessingcela peut Ă©galement augmenter la productivitĂ©, mais les coĂ»ts sont gĂ©nĂ©ralement plus Ă©levĂ©s que lors de l'utilisation multithreading.
  • L'existence de Python GIL nous montre clairement qu'Ă  tout moment dans un programme, un seul thread peut s'exĂ©cuter.
  • Dans les tĂąches liĂ©es au processeur, l'utilisation multithreadingpeut rĂ©duire les performances.
  • Dans les tĂąches liĂ©es au processeur, l'utilisation multiprocessingpeut amĂ©liorer les performances.
  • Les sorciers sont gĂ©niaux!

C'est lĂ  que nous terminerons notre introduction Ă  multithreadinget multiprocessingen Python aujourd'hui . Maintenant, allez gagner!



"Modélisation de COVID-19 à l'aide d'une analyse graphique et d'une analyse des données ouvertes." Leçon gratuite.

All Articles