Un guide du multitraitement Python et de la programmation parallèle

Accélérer les calculs est un objectif que tout le monde veut atteindre. Que se passe-t-il si vous avez un script qui pourrait s’exécuter dix fois plus vite que son temps d’exécution actuel ? Dans cet article, nous examinerons le multitraitement Python et une bibliothèque appelée multiprocessing
. Nous parlerons de ce qu’est le multitraitement, de ses avantages et de la manière d’améliorer le temps d’exécution de vos programmes Python en utilisant la programmation parallèle.
Bon, alors allons-y !
Une introduction au parallélisme
Avant de plonger dans le code Python, nous devons parler de traitement en parallèlequi est un concept important en informatique.
Habituellement, lorsque vous exécutez un script Python, votre code devient à un moment donné un processus et le processus s’exécute sur un seul cœur de votre CPU. Mais les ordinateurs modernes ont plus d’un cœur, alors que diriez-vous si vous pouviez utiliser plus de cœurs pour vos calculs ? Il s’avère que vos calculs seront plus rapides.
Prenons cela comme un principe général pour l’instant, mais plus tard, dans cet article, nous verrons que ce n’est pas universellement vrai.
Sans entrer dans trop de détails, l’idée derrière le parallélisme est d’écrire votre code de telle manière qu’il puisse utiliser plusieurs cœurs du CPU.
Pour faciliter les choses, regardons un exemple.
Calcul parallèle et série
Imaginez que vous avez un énorme problème à résoudre et que vous êtes seul. Vous devez calculer la racine carrée de huit nombres différents. Que fais-tu? Eh bien, vous n’avez pas beaucoup d’options. Vous commencez par le premier chiffre et vous calculez le résultat. Ensuite, vous continuez avec les autres.
Et si vous aviez trois amis bons en maths prêts à vous aider ? Chacun d’eux calculera la racine carrée de deux nombres, et votre travail sera plus facile car la charge de travail est répartie équitablement entre vos amis. Cela signifie que votre problème sera résolu plus rapidement.
Bon, alors tout est clair ? Dans ces exemples, chaque ami représente un cœur du CPU. Dans le premier exemple, la tâche entière est résolue séquentiellement par vous. C’est appelé informatique en série. Dans le deuxième exemple, puisque vous travaillez avec quatre cœurs au total, vous utilisez traitement en parallèle. L’informatique parallèle implique l’utilisation de processus parallèles ou de processus divisés entre plusieurs cœurs dans un processeur.
Modèles pour la programmation parallèle
Nous avons établi ce qu’est la programmation parallèle, mais comment l’utilisons-nous ? Eh bien, nous avons déjà dit que le calcul parallèle implique l’exécution de plusieurs tâches sur plusieurs cœurs du processeur, ce qui signifie que ces tâches sont exécutées simultanément. Il y a quelques questions que vous devriez considérer avant d’aborder la parallélisation. Par exemple, y a-t-il d’autres optimisations qui pourraient accélérer nos calculs ?
Pour l’instant, prenons pour acquis que la parallélisation est la meilleure solution pour vous. Il y a principalement trois des modèles en calcul parallèle :
- Parfaitement parallèle. Les tâches peuvent être exécutées indépendamment et n’ont pas besoin de communiquer entre elles.
- Parallélisme de la mémoire partagée. Les processus (ou threads) doivent communiquer, ils partagent donc un espace d’adressage global.
- Transmission de messages. Les processus doivent partager des messages en cas de besoin.
Dans cet article, nous allons illustrer le premier modèle, qui est aussi le plus simple.
Python Multiprocessing : Parallélisme basé sur les processus en Python
Une façon d’atteindre le parallélisme en Python est d’utiliser le module multitraitement. La multiprocessing
Le module vous permet de créer plusieurs processus, chacun d’eux avec son propre interpréteur Python. Pour cette raison, le multitraitement Python réalise un parallélisme basé sur les processus.
Vous avez peut-être entendu parler d’autres bibliothèques, comme threading
, qui est également intégré à Python, mais il existe des différences cruciales entre eux. La multiprocessing
module crée de nouveaux processus, tandis que threading
crée de nouveaux fils.
Dans la section suivante, nous examinerons les avantages de l’utilisation du multitraitement.
Avantages de l’utilisation du multitraitement
Voici quelques avantages du multitraitement :
- meilleure utilisation du processeur lors de tâches gourmandes en ressources processeur
- plus de contrôle sur un enfant par rapport aux threads
- facile à coder
Le premier avantage est lié aux performances. Étant donné que le multitraitement crée de nouveaux processus, vous pouvez mieux utiliser la puissance de calcul de votre processeur en répartissant vos tâches entre les autres cœurs. De nos jours, la plupart des processeurs sont des processeurs multicœurs, et si vous optimisez votre code, vous pouvez gagner du temps en résolvant des calculs en parallèle.
Le deuxième avantage concerne une alternative au multitraitement, qui est le multithreading. Les threads ne sont pas des processus cependant, et cela a ses conséquences. Si vous créez un thread, il est dangereux de le tuer ou même de l’interrompre comme vous le feriez avec un processus normal. Étant donné que la comparaison entre le multitraitement et le multithreading n’entre pas dans le cadre de cet article, je vous encourage à poursuivre votre lecture.
Le troisième avantage du multitraitement est qu’il est assez facile à mettre en œuvre, étant donné que la tâche que vous essayez de gérer est adaptée à la programmation parallèle.
Premiers pas avec le multitraitement Python
Nous sommes enfin prêts à écrire du code Python !
Nous allons commencer par un exemple très basique et nous l’utiliserons pour illustrer les aspects fondamentaux du multitraitement Python. Dans cet exemple, nous aurons deux processus :
- La
parent
traiter. Il n’y a qu’un seul processus parent, qui peut avoir plusieurs enfants. - La
child
traiter. Ceci est engendré par le parent. Chaque enfant peut aussi avoir de nouveaux enfants.
Nous allons utiliser le child
processus pour exécuter une certaine fonction. De cette façon, le parent
peut poursuivre son exécution.
Un exemple simple de multitraitement Python
Voici le code que nous utiliserons pour cet exemple :
from multiprocessing import Process
def bubble_sort(array):
check = True
while check == True:
check = False
for i in range(0, len(array)-1):
if array[i] > array[i+1]:
check = True
temp = array[i]
array[i] = array[i+1]
array[i+1] = temp
print("Array sorted: ", array)
if __name__ == '__main__':
p = Process(target=bubble_sort, args=([1,9,4,5,2,6,8,4],))
p.start()
p.join()
Dans cet extrait, nous avons défini une fonction appelée bubble_sort(array)
. Cette fonction est une implémentation vraiment naïve de l’algorithme de tri Bubble Sort. Si vous ne savez pas ce que c’est, ne vous inquiétez pas, car ce n’est pas si important. La chose cruciale à savoir est que c’est une fonction qui fait du travail.
La classe Processus
De multiprocessing
nous importons la classe Process
. Cette classe représente une activité qui sera exécutée dans un processus séparé. En effet, vous pouvez voir que nous avons passé quelques arguments :
target=bubble_sort
ce qui signifie que notre nouveau processus exécutera lebubble_sort
fonctionargs=([1,9,4,52,6,8,4],)
qui est le tableau passé en argument à la fonction cible
Une fois que nous avons créé une instance de la classe Process, nous n’avons qu’à démarrer le processus. Cela se fait en écrivant p.start()
. À ce stade, le processus est lancé.
Avant de quitter, nous devons attendre que le processus enfant termine ses calculs. La join()
La méthode attend que le processus se termine.
Dans cet exemple, nous avons créé un seul processus enfant. Comme vous pouvez le deviner, nous pouvons créer plus de processus enfants en créant plus d’instances dans le Process
classer.
La classe Piscine
Que se passe-t-il si nous devons créer plusieurs processus pour gérer des tâches plus gourmandes en CPU ? Avons-nous toujours besoin de commencer et d’attendre explicitement la fin ? La solution ici est d’utiliser le Pool
classer.
La Pool
La classe vous permet de créer un pool de processus de travail, et dans l’exemple suivant, nous verrons comment l’utiliser. Voici notre nouvel exemple :
from multiprocessing import Pool
import time
import math
N = 5000000
def cube(x):
return math.sqrt(x)
if __name__ == "__main__":
with Pool() as pool:
result = pool.map(cube, range(10,N))
print("Program finished!")
Dans cet extrait de code, nous avons un cube(x)
fonction qui prend simplement un entier et renvoie sa racine carrée. Facile, non ?
Ensuite, nous créons une instance de Pool
class, sans spécifier d’attribut. La classe pool crée par défaut un processus par cœur de processeur. Ensuite, nous exécutons le map
méthode avec quelques arguments.
La map
méthode applique la cube
fonction à chaque élément de l’itérable que nous fournissons – qui, dans ce cas, est une liste de tous les nombres de 10
à N
.
L’énorme avantage est que les calculs sur la liste se font en parallèle !
Tirer le meilleur parti du multitraitement Python
Créer plusieurs processus et effectuer des calculs parallèles n’est pas nécessairement plus efficace que le calcul en série. Pour les tâches à faible consommation de CPU, le calcul en série est plus rapide que le calcul en parallèle. Pour cette raison, il est important de comprendre quand vous devez utiliser le multitraitement, qui dépend des tâches que vous effectuez.
Pour vous en convaincre, regardons un exemple simple :
from multiprocessing import Pool
import time
import math
N = 5000000
def cube(x):
return math.sqrt(x)
if __name__ == "__main__":
start_time = time.perf_counter()
with Pool() as pool:
result = pool.map(cube, range(10,N))
finish_time = time.perf_counter()
print("Program finished in {} seconds - using multiprocessing".format(finish_time-start_time))
print("---")
start_time = time.perf_counter()
result = []
for x in range(10,N):
result.append(cube(x))
finish_time = time.perf_counter()
print("Program finished in {} seconds".format(finish_time-start_time))
Cet extrait est basé sur l’exemple précédent. Nous résolvons le même problème, qui consiste à calculer la racine carrée de N
chiffres, mais de deux manières. Le premier implique l’utilisation du multitraitement Python, tandis que le second ne le fait pas. Nous utilisons le perf_counter()
méthode de la time
bibliothèque pour mesurer les performances temporelles.
Sur mon portable, j’obtiens ce résultat :
> python code.py
Program finished in 1.6385094 seconds - using multiprocessing
---
Program finished in 2.7373942999999996 seconds
Comme vous pouvez le voir, il y a plus d’une seconde de différence. Donc dans ce cas, le multitraitement est préférable.
Modifions quelque chose dans le code, comme la valeur de N
. Baissons-le à N=10000
et voyez ce qui se passe.
Voici ce que j’obtiens maintenant :
> python code.py
Program finished in 0.3756742 seconds - using multiprocessing
---
Program finished in 0.005098400000000003 seconds
Qu’est-il arrivé? Il semble que le multitraitement soit maintenant un mauvais choix. Pourquoi?
La surcharge introduite par le fractionnement des calculs entre les processus est trop importante par rapport à la tâche résolue. Vous pouvez voir combien il y a de différence en termes de performances temporelles.
Conclusion
Dans cet article, nous avons parlé de l’optimisation des performances du code Python en utilisant le multitraitement Python.
Tout d’abord, nous avons brièvement présenté ce qu’est le calcul parallèle et les principaux modèles pour l’utiliser. Ensuite, nous avons commencé à parler du multitraitement et de ses avantages. Au final, nous avons vu que paralléliser les calculs n’est pas toujours le meilleur choix et la multiprocessing
module doit être utilisé pour paralléliser les tâches liées au processeur. Comme toujours, il s’agit de considérer le problème spécifique auquel vous êtes confronté et d’évaluer les avantages et les inconvénients des différentes solutions.
J’espère que vous avez trouvé l’apprentissage du multitraitement Python aussi utile que moi.
Source link