Contactez-nous

Introduction au multiprocessing (module multiprocessing)

Découvrez le module 'multiprocessing' en Python pour contourner les limitations du GIL et exécuter du code Python en parallèle sur plusieurs coeurs de processeur. Créez et gérez des processus, et tirez parti de la puissance de votre matériel.

Multiprocessing vs. Multithreading : processus vs. threads

Le module `multiprocessing` permet de créer et de gérer des *processus*, alors que le module `threading` permet de créer et de gérer des *threads*. Il est important de comprendre la différence entre les deux :

  • Thread : Un thread est une séquence d'instructions qui s'exécute au sein d'un même processus. Tous les threads d'un processus partagent le même espace mémoire. En Python (CPython), le GIL limite le parallélisme des threads pour le code Python.
  • Processus : Un processus est une instance d'un programme en cours d'exécution. Chaque processus a son propre espace mémoire isolé. Les processus sont plus lourds que les threads (ils consomment plus de ressources), mais ils permettent un vrai parallélisme, même pour le code Python (car chaque processus a son propre interpréteur Python et son propre GIL).

Le `multiprocessing` contourne la limitation du GIL en créant des processus distincts, chacun avec son propre interpréteur Python. Cela permet d'exécuter du code Python en *parallèle* sur plusieurs coeurs de processeur.

Le `multiprocessing` est donc adapté aux tâches *CPU-bound* (limitées par la vitesse du processeur), contrairement au `threading` qui est plus adapté aux tâches *I/O-bound* (limitées par la vitesse des entrées/sorties).

Créer un processus : la classe Process

Pour créer un processus, vous utilisez la classe `Process` du module `multiprocessing`. La syntaxe est similaire à celle de la classe `Thread` du module `threading`.

Syntaxe :

import multiprocessing

# Définir la fonction que le processus exécutera
def ma_fonction(arguments):
    # Code à exécuter dans le processus
    # ...

# Créer un objet Process
processus = multiprocessing.Process(target=ma_fonction, args=(arg1, arg2, ...), kwargs={...})

# Démarrer le processus
processus.start()

# Attendre que le processus se termine (optionnel)
processus.join()
  • `target=ma_fonction` : Spécifie la fonction que le processus doit exécuter.
  • `args=(arg1, arg2, ...)` : (Optionnel) Spécifie les arguments positionnels à passer à la fonction.
  • `kwargs={...}` : (Optionnel) Spécifie les arguments nommés à passer à la fonction.
  • `processus.start()` : Démarre l'exécution du processus. La fonction `ma_fonction` sera exécutée dans un processus séparé.
  • `processus.join()` : (Optionnel) Attend que le processus se termine.

Exemple :

import multiprocessing
import time
import os

def afficher_id_processus():
    print(f"ID du processus : {os.getpid()}")

if __name__ == '__main__':
  print(f"ID du processus principal : {os.getpid()}")
  # Créer un processus
  processus = multiprocessing.Process(target=afficher_id_processus)

  # Démarrer le processus
  processus.start()

  # Attendre que le processus se termine
  processus.join()

  print("Fin du programme")

Dans cet exemple, la fonction `afficher_id_processus` affiche l'ID du processus courant (obtenu avec `os.getpid()`). Le processus principal crée un nouveau processus qui exécute cette fonction. Vous verrez que les IDs des processus sont différents, ce qui montre qu'ils s'exécutent dans des processus séparés.

Note importante: Le bloc `if __name__ == '__main__':` est crucial lorsque vous utilisez `multiprocessing`. Il permet d'éviter que le code de création des processus ne soit exécuté par les processus enfants (ce qui créerait une boucle infinie de processus). Placez toujours le code qui crée et démarre les processus à l'intérieur de ce bloc.

Exemple : calcul en parallèle

Voici un exemple plus concret, qui montre comment utiliser `multiprocessing` pour effectuer un calcul en parallèle :

import multiprocessing
import time

def calculer_carres(nombres):
    """Calcule les carrés d'une liste de nombres."""
    resultats = []
    for nombre in nombres:
        resultats.append(nombre * nombre)
    print(f"Résultats (processus {multiprocessing.current_process().name}): {resultats}")


if __name__ == '__main__':
    nombres = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

    # Créer deux processus
    processus1 = multiprocessing.Process(target=calculer_carres, args=(nombres[:5],), name = "Calcul1") #Le nom est optionnel
    processus2 = multiprocessing.Process(target=calculer_carres, args=(nombres[5:],), name = "Calcul2")

    # Démarrer les processus
    processus1.start()
    processus2.start()

    # Attendre que les processus se terminent
    processus1.join()
    processus2.join()

    print("Calcul terminé.")

Dans cet exemple :

  • La fonction `calculer_carres` calcule les carrés d'une liste de nombres.
  • On crée deux processus : `processus1` qui calculera les carrés des 5 premiers nombres, et `processus2` qui calculera les carrés des 5 derniers nombres.
  • Les processus sont démarrés avec `start()`.
  • On attend que les processus se terminent avec `join()`.

Les deux processus s'exécutent *en parallèle* (si vous avez au moins deux coeurs de processeur), ce qui peut réduire le temps d'exécution total par rapport à une exécution séquentielle.

Notez que les processus ne partagent pas la mémoire. Si vous voulez qu'ils communiquent ou partagent des données, vous devez utiliser des mécanismes de communication inter-processus (IPC), comme les queues (`multiprocessing.Queue`) ou les tuyaux (`multiprocessing.Pipe`) (voir sections suivantes).

Communiquer entre les processus : multiprocessing.Queue

Les processus ne partagent pas le même espace mémoire, contrairement aux threads. Pour faire communiquer des processus entre eux (échanger des données, synchroniser leur exécution), vous devez utiliser des mécanismes de communication inter-processus (IPC).

Le module `multiprocessing` fournit plusieurs outils pour la communication inter-processus, dont les plus courants sont :

  • `Queue` : Une file d'attente (FIFO : First In, First Out) qui permet d'échanger des objets Python entre les processus. Un processus peut mettre des objets dans la file d'attente avec `put()`, et un autre processus peut les récupérer avec `get()`.
  • `Pipe` : Un tuyau (pipe) qui permet une communication bidirectionnelle entre deux processus. Un tuyau a deux extrémités (`Connection` objects), et chaque extrémité a des méthodes `send()` et `recv()` pour envoyer et recevoir des objets.

Exemple (avec `Queue`) :

import multiprocessing

def producteur(queue):
    """Met des éléments dans la file d'attente."""
    for i in range(5):
        queue.put(i)
        print(f"Processus producteur a mis : {i}")
    queue.put(None)  # Signal de fin

def consommateur(queue):
    """Prend des éléments de la file d'attente et les affiche."""
    while True:
        element = queue.get()
        if element is None:
            break  # Fin du traitement
        print(f"Processus consommateur a reçu : {element}")

if __name__ == '__main__':
    # Créer une file d'attente partagée
    queue = multiprocessing.Queue()

    # Créer les processus
    p1 = multiprocessing.Process(target=producteur, args=(queue,))
    p2 = multiprocessing.Process(target=consommateur, args=(queue,))

    # Démarrer les processus
    p1.start()
    p2.start()

    # Attendre que les processus se terminent
    p1.join()
    p2.join()

Dans cet exemple :

  • Une file d'attente (`multiprocessing.Queue`) est créée pour permettre la communication entre les processus.
  • Un processus "producteur" met des éléments dans la file d'attente.
  • Un processus "consommateur" prend des éléments de la file d'attente et les traite.
  • Un signal de fin (`None`) est utilisé pour indiquer au consommateur qu'il n'y a plus d'éléments à traiter.

Les files d'attente et les tuyaux sont des mécanismes de communication inter-processus *thread-safe* et *process-safe*.

Pool de processus

La classe `Pool` permet de créer un ensemble de processus et de distribuer des tâches. Elle est très utile lorsque l'on veut paralléliser l'exécution d'une fonction sur un ensemble de données :

from multiprocessing import Pool

def carre(n):
  return n * n

if __name__ == '__main__':
  nombres = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

  with Pool(processes=4) as pool: # Crée un pool de 4 processus
    resultats = pool.map(carre, nombres) #Distribue le calcul des carrés sur les processus du pool

print(resultats) #[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

`Pool` propose des méthodes comme `map`, `apply_async`, `starmap` qui permettent de distribuer les tâches facilement.

Bonnes pratiques et considérations

Voici quelques bonnes pratiques et considérations lors de l'utilisation de `multiprocessing` :

  • Utilisez `if __name__ == '__main__'` : Protégez le code qui crée et démarre les processus avec ce bloc conditionnel.
  • Choisissez le bon mécanisme de communication : Utilisez des queues, des tuyaux, ou d'autres mécanismes de communication inter-processus pour échanger des données entre les processus. Evitez de partager directement des objets en mémoire (sauf si vous utilisez des objets spécifiques conçus pour cela, comme `multiprocessing.Value` ou `multiprocessing.Array`).
  • Gérez les erreurs : Les exceptions levées dans un processus ne sont pas automatiquement propagées au processus parent. Vous devez gérer les exceptions explicitement (par exemple, en utilisant une queue pour communiquer les erreurs).
  • Evitez les blocages (deadlocks) : Soyez prudent lorsque vous utilisez des verrous ou d'autres mécanismes de synchronisation entre les processus, pour éviter les situations de blocage.
  • Gardez à l'esprit le surcoût : Créer et gérer des processus est plus coûteux que de créer et gérer des threads. N'utilisez `multiprocessing` que si le gain en performance (dû au parallélisme) compense le surcoût.
  • Testez votre code : Le code multiprocessus est plus difficile à déboguer que le code séquentiel. Testez soigneusement votre code, en particulier les aspects liés à la communication et à la synchronisation.
  • Surveillez l'utilisation des ressources : Le multiprocessing peut consommer beaucoup de ressources CPU et mémoire. Surveillez l'utilisation des ressources de votre programme.

Le multiprocessing est un outil puissant pour exploiter le parallélisme en Python, mais il doit être utilisé avec discernement et avec une bonne compréhension de ses mécanismes et de ses limitations.