Multithreading et Multiprocessing en Python : Guide complet avec exemples

Optimisez vos applications Python avec le multithreading et multiprocessing. Comprenez le GIL, le partage de ressources et la synchronisation.

Mahmoud DEVO
Mahmoud DEVO
December 27, 2025 7 min read
Multithreading et Multiprocessing en Python : Guide complet avec exemples

Multithreading et Multiprocessing en Python : Partage de Ressources et Performance Optimale

Introduction

Les developpeurs Python sont souvent confrontes a la necessite d’optimiser les performances de leurs applications. L’une des approches les plus efficaces consiste a utiliser les threads et les processus pour permettre a plusieurs taches de s’executer simultanement.

Dans le monde moderne du developpement, la programmation concurrente est devenue indispensable. Que vous construisiez une API web qui doit gerer des milliers de requetes, un script de scraping qui doit parcourir des centaines de pages, ou une application de traitement de donnees qui doit analyser des gigaoctets d’informations, la maitrise du multithreading et du multiprocessing est essentielle.

Dans cet article, nous explorerons les concepts cles du multithreading et du multiprocessing en Python, ainsi que les meilleures pratiques pour partager les ressources entre ces differentes unites de travail. Nous verrons egalement les pieges courants a eviter et les patterns recommandes par la communaute Python.

Le Global Interpreter Lock (GIL)

Avant de plonger dans le monde des threads, il est important de comprendre la notion de GIL. Le GIL est un mecanisme qui empeche plusieurs threads d’executer du code Python simultanement. Bien que cela puisse sembler contre-intuitif, le GIL a ete mis en place pour garantir l’integrite des operations de la memoire et des objets Python.

Le GIL est specifique a l’implementation CPython (l’implementation standard de Python). D’autres implementations comme Jython ou IronPython n’ont pas cette limitation.

import threading
import time

def fonction_lente():
    time.sleep(2)

# Execution d'une seule thread
start = time.time()
fonction_lente()
print("Temps d'execution (1 thread) : %.2fs" % (time.time() - start))

# Execution de plusieurs threads
start = time.time()
threads = [threading.Thread(target=fonction_lente) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print("Temps d'execution (4 threads) : %.2fs" % (time.time() - start))

Dans cet exemple, les 4 threads s’executent en parallele pour les operations I/O (comme time.sleep), donc le temps total est d’environ 2 secondes et non 8. C’est parce que le GIL est relache pendant les operations I/O.

Le Multithreading

Le multithreading permet aux programmes d’executer plusieurs taches en parallele. Les threads partagent le meme espace memoire, ce qui les rend ideaux pour les taches I/O-bound (lecture de fichiers, requetes reseau, etc.).

Exemple basique de creation de threads

import threading
import time

def fonction_thread(nom):
    print(f"Thread {nom} demarre")
    time.sleep(1)
    print(f"Thread {nom} termine")

# Creation de threads
threads = [threading.Thread(target=fonction_thread, args=(i,)) for i in range(3)]

# Demarrage des threads
for t in threads:
    t.start()

# Attente de la fin de tous les threads
for t in threads:
    t.join()

print("Tous les threads sont termines")

Utilisation de ThreadPoolExecutor (recommande)

Pour une gestion plus elegante des threads, utilisez concurrent.futures :

from concurrent.futures import ThreadPoolExecutor
import requests

def telecharger_page(url):
    response = requests.get(url)
    return len(response.content)

urls = [
    "https://python.org",
    "https://github.com",
    "https://stackoverflow.com"
]

# Execution parallele avec un pool de threads
with ThreadPoolExecutor(max_workers=3) as executor:
    resultats = list(executor.map(telecharger_page, urls))

print(f"Tailles des pages : {resultats}")

Le Multiprocessing

Le multiprocessing permet aux programmes d’executer plusieurs processus en parallele. Contrairement aux threads, chaque processus a son propre espace memoire et son propre GIL. C’est la solution ideale pour les taches CPU-bound (calculs intensifs).

Exemple basique de creation de processus

import multiprocessing
import os

def fonction_process(nom):
    print(f"Process {nom} - PID: {os.getpid()}")
    # Simulation d'un calcul intensif
    resultat = sum(i * i for i in range(10_000_000))
    print(f"Process {nom} termine - Resultat: {resultat}")

if __name__ == "__main__":
    # Creation de processus
    processes = [multiprocessing.Process(target=fonction_process, args=(i,)) for i in range(4)]

    # Demarrage des processus
    for p in processes:
        p.start()

    # Attente de la fin de tous les processus
    for p in processes:
        p.join()

    print("Tous les processus sont termines")

Utilisation de ProcessPoolExecutor (recommande)

from concurrent.futures import ProcessPoolExecutor
import math

def calcul_intensif(n):
    return sum(math.sqrt(i) for i in range(n))

nombres = [10_000_000, 20_000_000, 15_000_000, 25_000_000]

if __name__ == "__main__":
    with ProcessPoolExecutor(max_workers=4) as executor:
        resultats = list(executor.map(calcul_intensif, nombres))

    print(f"Resultats : {resultats}")

Partage de Ressources et Synchronisation

Partage entre Threads avec Lock

Lorsque plusieurs threads partagent des ressources, il est crucial de prendre en compte les problemes de synchronisation. Utilisez un Lock pour eviter les conditions de course :

import threading

compteur = 0
lock = threading.Lock()

def incrementer():
    global compteur
    for _ in range(100_000):
        with lock:  # Protection de la section critique
            compteur += 1

threads = [threading.Thread(target=incrementer) for _ in range(4)]

for t in threads:
    t.start()
for t in threads:
    t.join()

print(f"Compteur final : {compteur}")  # Toujours 400000

Partage entre Processus avec Value et Array

Les processus ne partagent pas la memoire par defaut. Utilisez multiprocessing.Value ou multiprocessing.Array :

import multiprocessing

def incrementer(compteur, lock):
    for _ in range(100_000):
        with lock:
            compteur.value += 1

if __name__ == "__main__":
    compteur = multiprocessing.Value('i', 0)  # 'i' = integer
    lock = multiprocessing.Lock()

    processes = [multiprocessing.Process(target=incrementer, args=(compteur, lock)) for _ in range(4)]

    for p in processes:
        p.start()
    for p in processes:
        p.join()

    print(f"Compteur final : {compteur.value}")

Communication avec Queue

Pour une communication plus complexe entre threads ou processus, utilisez une Queue :

from multiprocessing import Process, Queue

def producteur(queue):
    for i in range(5):
        queue.put(f"Message {i}")
    queue.put(None)  # Signal de fin

def consommateur(queue):
    while True:
        message = queue.get()
        if message is None:
            break
        print(f"Recu : {message}")

if __name__ == "__main__":
    queue = Queue()
    p1 = Process(target=producteur, args=(queue,))
    p2 = Process(target=consommateur, args=(queue,))

    p1.start()
    p2.start()
    p1.join()
    p2.join()

Bonnes Pratiques

Choisir entre Threading et Multiprocessing

SituationRecommandation
Taches I/O-bound (reseau, fichiers)Threading ou asyncio
Taches CPU-bound (calculs)Multiprocessing
Besoin de partager beaucoup de donneesThreading
Isolation complete necessaireMultiprocessing

Regles d’or

  1. Utilisez les context managers : Preferez with ThreadPoolExecutor() as executor: pour une gestion automatique des ressources.

  2. Limitez le nombre de workers : Un bon point de depart est os.cpu_count() pour le multiprocessing et os.cpu_count() * 5 pour le threading.

  3. Protegez les sections critiques : Toujours utiliser des locks pour les ressources partagees.

  4. Evitez les variables globales : Passez les donnees explicitement aux fonctions.

# Bonne pratique : passage explicite
def traiter(donnees, resultat_queue):
    resultat_queue.put(process(donnees))

# A eviter : variable globale
donnees_globales = []
def traiter():
    global donnees_globales  # Problematique !

Pieges Courants

1. Oublier if __name__ == "__main__"

Sur Windows et macOS, le multiprocessing necessite cette protection :

# INCORRECT - peut causer une boucle infinie
from multiprocessing import Process

def worker():
    print("Working")

p = Process(target=worker)
p.start()  # Erreur sur Windows !

# CORRECT
if __name__ == "__main__":
    p = Process(target=worker)
    p.start()

2. Conditions de course (Race Conditions)

# DANGEREUX - condition de course
compteur = 0
def incrementer():
    global compteur
    temp = compteur
    temp += 1
    compteur = temp  # Autre thread peut avoir modifie compteur !

# SECURISE - avec lock
lock = threading.Lock()
def incrementer_safe():
    global compteur
    with lock:
        compteur += 1

3. Deadlocks

Evitez d’acquerir plusieurs locks dans un ordre different :

# RISQUE DE DEADLOCK
def fonction_a():
    with lock_1:
        with lock_2:  # Thread B peut avoir lock_2 et attendre lock_1
            pass

def fonction_b():
    with lock_2:
        with lock_1:  # Deadlock !
            pass

# SOLUTION : toujours acquerir les locks dans le meme ordre

4. Ne pas joindre les threads/processus

Toujours appeler join() pour attendre la fin des workers :

threads = [threading.Thread(target=work) for _ in range(4)]
for t in threads:
    t.start()

# NE PAS OUBLIER !
for t in threads:
    t.join()  # Attendre que tous les threads terminent

Conclusion

Le multithreading et le multiprocessing sont des outils puissants pour optimiser les performances de vos applications Python. Voici les points cles a retenir :

  • Threading : Ideal pour les taches I/O-bound grace au relachement du GIL pendant les operations I/O
  • Multiprocessing : Necessaire pour les taches CPU-bound car chaque processus a son propre GIL
  • Synchronisation : Indispensable pour eviter les conditions de course avec les Locks, Semaphores et Queues
  • concurrent.futures : API moderne et elegante pour gerer les pools de threads et processus

La maitrise de ces concepts vous permettra de construire des applications performantes et robustes. N’hesitez pas a experimenter avec les differents patterns presentes dans cet article pour trouver la solution optimale pour votre cas d’usage.

Pour aller plus loin

  • Explorez asyncio pour la programmation asynchrone
  • Decouvrez multiprocessing.Manager pour le partage d’objets complexes
  • Etudiez les patterns Producer-Consumer et Worker Pool
  • Testez vos implementations avec des outils de profiling comme cProfile
Advertisement

In-Article Ad

Dev Mode

Share this article

Mahmoud DEVO

Mahmoud DEVO

Senior Full-Stack Developer

I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.

Enjoyed this article?

Subscribe to get more tech content delivered to your inbox.

Related Articles