Bonjour à vous. Aujourd'hui, on s'intéresse à la programmation parallèle grâce au module multiprocessing
en Python.
Introduction :
La programmation parallèle permet d'effectuer plusieurs tâches de façon simultanée, permettant ainsi un gain de temps colossal.
Mise en situation :
Supposez qu'aujourd'hui, une longue journée vous attende. Vous devez :
- réceptionner un colis,
- faire le ménage,
- rédiger un mail,
- cuisiner un gâteau,
- mettre des lessives en route.
Vous n'allez pas attendre qu'une tâche soit terminée pour en faire une autre.
D'abord, vous allez mettre des lessives en route. En même temps, vous allez faire le ménage. Vous entendez quelqu'un toquer à la porte : vous réceptionnez votre colis.
Enfin, vous commencez à préparer votre gâteau et pendant qu'il cuit, vous rédigez votre mail.
Vous avez effectuer plusieurs tâches en simultané. C'est exactement ce que nous allons faire en Python avec le module multiprocessing
.
À l'abordage :
Le module multiprocessing
va créer un processus enfant
au processus parent
(votre programme principal) qui s'occupera d'effectuer les opérations en parallèle.
Si vous avez déjà utilisé l'appel système fork
en C, les notions que nous allons aborder vous sembleront plutôt simple à comprendre.
Pour illustrer ce schéma parent/enfant
, voici un exemple d'utilisation :
import os
import time
def ma_fonction(numero_processus_enfant: int):
""" Fonction appelée par le processus `enfant` """
# Pause de 5 secondes pour simuler une tâche chronophage.
time.sleep(5)
# Affichage de :
# Numéro de création du processus enfant
# Identifiant du processus parent
# Identifiant du processus enfant
print(
f"Processus enfant numéro {numero_processus_enfant} :\n"
f"\tID du processus parent : {os.getppid()}\n"
f"\tID du processus enfant {os.getpid()}\n"
)
Le but de la fonction ma_fonction
est de simuler une tâche chronophage dont on souhaiterait parallèliser l'exécution.
Après le traitement long d'une tâche, elle affiche le numéro du processus enfant
entrain de l'exécuter, ainsi que l'identifiant du processus parent
et l'identifiant du processus enfant
.
L'identifiant du processus enfant
n'est pas à confondre avec le numéro du processus enfant
, ce dernier correspondant à l'ordre dans lequel le processus a été initialisé.
from multiprocessing import Process
def main():
processus = [Process(target=ma_fonction, args=(i,)) for i in range(4)]
for p in processus:
p.start()
for p in processus:
p.join()
La fonction main
créée une liste de 4 processus. Chacun d'entre eux devra exécuter la fonction ma_fonction
en lui passant le paramètre i
dans la bouche, qui correspond au numéro de création d'un processus.
Une fois cette liste créée, on la parcours. Pour chaque processus rencontré, on le lance.
On parcours une seconde fois la liste et, pour chaque élément, on appelle la méthode join
. Cet appel nous permet d'attendre la fin d'exécution du processus avant de retourner à l'exécution linéaire.
Enfin, pour éviter une erreur de type RuntimeError
, due à une mauvaise gestion de la fonction fork
appelée à l'intérieur du module multiprocessing
, nous devons rajouter l'emblématique :
if __name__ == "__main__":
main()
Si vous exécutez le programme, au bout de 5 secondes, vous devriez avoir un affichage similaire à celui-ci :
Processus enfant numéro 1 :
ID du processus parent : 20340
ID du processus enfant 20343
Processus enfant numéro 2 :
ID du processus parent : 20340
ID du processus enfant 20344
Processus enfant numéro 3 :
ID du processus parent : 20340
ID du processus enfant 20345
Processus enfant numéro 0 :
ID du processus parent : 20340
ID du processus enfant 20342
Les valeurs des identifiants et l'ordre des numéros d'enfant
devraient être différents de ceux que vous voyez ici.
Ce qui est important c'est de remarquer que chaque "ID du processus enfant" est rattaché au même "ID du processus parent", puisque c'est le même programme qui a initialisé les 4 processus.
L'avantage de l'utilisation des processus enfant
ici est que le coût en temps des tâches est divisé par 4.
Si nous avions exécuté de façon linéaire les instructions, nous aurions du attendre environ 5 secondes x 4 exécutions de la fonction ma_fonction
, soit 20 secondes.
Pour les personnes ayant déjà utilisé le module threading
, les méthodes start
et join
vous semblent familière, de même que les arguments target
et args
pour la fonction multiprocessing.Process
et c'est tout à fait normal. L'API de multiprocessing
essaie d'aborder les mêmes concepts pour faciliter la transposition entre les deux APIs.
L'object multiprocessing.Pool
:
Nous avons vu comment créer un processus enfant
pour faire nos calculs, mais cette tâche demande de la réflexion et de l'organisation.
Supposons que nous avons une grande liste à traiter. Nous devons appliquer une fonction à chaque éléments de la liste. Rapidement, on pourrait penser à utiliser la fonction map
qui répond exactement à notre demande.
Cependant, nous pouvons initialiser un objet multiprocessing.Pool
qui va gérer un nombre de processus déterminé, sans que nous ayons besoin d'intervenir, et qui répartira les tâches de calculs entre ces processus.
Voici un petit exemple :
def ma_fonction(nombre: int):
""" Fonction appelée par la fonction `map` de l'object `Pool` """
return nombre ** nombre
Cette fonction sera appliquée à chaque élément d'une liste quelconque.
import time
from multiprocessing import Pool, cpu_count
def main():
ma_liste = range(10_000)
with Pool(processes=cpu_count() - 1) as pool:
debut = time.perf_counter()
ma_liste_traitee = pool.map(ma_fonction, ma_liste)
fin = time.perf_counter()
print(f"Traitement de la liste avec les processus enfants terminé en {fin - debut:.2f} secondes.")
_debut = time.perf_counter()
_ma_liste_traitee = list(map(ma_fonction, ma_liste))
_fin = time.perf_counter()
print(f"Traitement de la liste sans les processus enfants terminé en {_fin - _debut:.2f} secondes.")
assert _ma_liste_traitee == ma_liste_traitee, "Les deux listes sont différentes."
print("Les deux listes traitées sont égales.")
Ici, nous créons une liste ma_liste
de 10 000 nombres de 0 à 9 999.
On créé un objet Pool
que vous pourriez vous représenter comme une piscine dans laquelle nagent des processus qui attendent d'être utilisés. Pour créer l'objet Pool
, vous pouvez passer un argument optionnel appelé processes
qui représente le nombre de processus qui pourront être appelés pour effectuer les calculs.
Faites bien attention à mettre une valeur relativement basse, au risque d'observer des ralentissements de votre ordinateur, pouvant aller jusqu'à devoir le redémarrer.
Vous devriez pouvoir faire comme dans cet exemple, et utiliser la fonction cpu_count()
pour obtenir le nombre de processus utilisables sur votre ordinateur, et y soustraire 1 pour assurer un minimum vos arrières.
Une fois créé, on se sert de la pool
pour appliquer une fonction map
à notre liste de nombre ma_liste
de telle sorte à obtenir une nouvelle liste avec le résultat de toutes les opérations ma_fonction
.
On stock le temps d'exécution de la méthode pool.map
avec la fonction time.perf_counter
, et on l'affiche.
Enfin, pour comparer, on réitère l'opération mais cette fois-ci, comme on l'aurait fait en programmant de façon linéaire, c'est-à-dire sans utiliser de multiprocessing.
Pour ça, on peut utiliser la builtin map
et lui passer la fonction à appliquer (ma_fonction
) ainsi que la liste sur laquelle itérer (ma_liste
).
Comme au dessus, on stock le temps d'exécution avec time.perf_counter
.
On compare ensuite les deux listes pour vérifier qu'elles sont bien égales.
Bien sûr, comme tout à l'heure, on rajoute :
if __name__ == "__main__":
main()
Pour ne pas avoir de soucis avec l'exécution de la fonction fork
.
Voici un résultat possible :
Traitement de la liste avec les processus enfants terminé en 1.43 secondes.
Traitement de la liste sans les processus enfants terminé en 5.85 secondes.
Les deux listes traitées sont égales.
On remarque assez rapidement que le traitement de la liste avec les processus enfants
s'est terminée 4 fois plus rapidement que le traitement sans les processus enfants
.
Conclusion :
Vous voilà armé avec les outils de multiprocessing
pour faire de la programmation parallèle. Vous pourrez diviser vos tâches en processus enfants
pour obtenir vos résultats plus rapidement.
Ce module peut faire penser au module threading
, que je couvrirai plus tard, mais il faut retenir que multiprocessing
utilise des processus supplémentaires pour fonctionner, alors que le module threading
utilise des threads
.
Faites bien attention lorsque vous utilisez le module multiprocessing
de ne pas surcharger le nombre de processus que vous souhaitez utiliser, notamment dans l'initialisation de l'objet Pool
.
Si vous mettez trop de processus, vous risquez d'observer un fort ralentissement de votre ordinateur pouvant vous amener à devoir le redémarrer.
Si vous souhaitez en apprendre davantage sur le module, vous pouvez consulter sa documentation à l'URL suivante : https://docs.python.org/3/library/multiprocessing.html
Top comments (0)