Skip to content

Concurrence, asynchrone et multithreading

💡 Guide d'apprentissage : La programmation concurrente est le "talon d'Achille" de nombreux ingénieurs backend — ils sont mis en difficulté lors des entretiens, rencontrent des bugs en production, et manquent d'idées pour l'optimisation des performances. Ce chapitre s'articule autour d'une question centrale : lorsque 100 000 utilisateurs sollicitent votre service simultanément, votre code va-t-il planter ?

Avant de commencer, il est conseillé de consolider deux "briques fondamentales" :

  • Qu'est-ce que le CPU, la mémoire et les E/S : si vous n'êtes pas familier avec ces concepts de base, vous pouvez d'abord revoir les connaissances fondamentales des systèmes d'exploitation.
  • Qu'est-ce que le blocage/non-blocage : si vous n'êtes pas encore familier avec les concepts de synchrone/asynchrone, vous pouvez d'abord en faire l'expérience par la programmation pratique.

0. Introduction : pourquoi votre service se "fige" lors des pics de trafic ?

Process / Thread / Coroutine Comparison

Memory Usage
400
MB
Context Switches
0
Completed Tasks
0
Elapsed Time
0
ms
CPU 1
CPU 2
CPU 3
CPU 4
Task Queue
Task 1
Task 2
Task 3
Task 4
Task 5
Task 6
Task 7
Task 8
Task 9
Task 10
Task 11
Task 12
Task 13
Task 14
Task 15
Task 16

Beaucoup de développeurs rencontrent des situations similaires dans la pratique :

  • Le service répond rapidement en test local, mais devient "saccadé comme un diaporama" une fois en ligne ;
  • Vous avez acheté un serveur avec une configuration élevée, mais l'utilisation du CPU ne monte jamais ;
  • Lors des pics de promotion, le service subit un "effondrement en cascade", obligeant à la dégradation ou au disjoncteur.

Intuitivement, on pense que : "le serveur n'est pas assez puissant". Mais la plupart du temps, le problème ne réside pas dans le fait que le matériel n'est "pas assez rapide", mais dans le fait que nous n'avons pas bien conçu le modèle de concurrence.

Contradiction centrale :

  • Sans traitement concurrent : les requêtes des utilisateurs s'accumulent en file d'attente, l'expérience est désastreuse ;
  • Avec un multithreading mal maîtrisé : compétition de verrous, surcoût des changements de contexte, les performances chutent au contraire.

Face à ces défis, se contenter d'"ajouter des machines" ne suffit plus. Nous avons besoin d'une méthode systématique de conception concurrente, qui garantit à la fois les performances et la stabilité dans les scénarios de haute concurrence. C'est précisément ce que ce chapitre tente de résoudre.


1. Concepts fondamentaux : processus, threads, coroutines, quelles différences ?

1.1 L'analogie du restaurant

Imaginez que vous gérez un restaurant et devez servir de nombreux clients simultanément :

ConceptAnalogie du restaurantSignification technique
Processus (Process)Une succursale indépendante du restaurantDispose d'un espace mémoire indépendant et de ressources allouées, c'est l'unité de base d'allocation des ressources du système d'exploitation. Un processus qui plante n'affecte pas les autres processus.
Thread (Thread)Un cuisinier dans la succursaleC'est l'unité de base d'ordonnancement du CPU, partage l'espace mémoire du processus. Les threads d'un même processus peuvent partager des données, mais le plantage d'un thread peut entraîner le plantage de tout le processus.
Coroutine (Coroutine)Le "don d'ubiquité" du cuisinierThread léger en espace utilisateur, ordonnancé par le programme lui-même plutôt que par le système d'exploitation. Le surcoût de commutation est extrêmement faible, on peut en créer des millions.

1.2 Comparaison approfondie : les différences essentielles entre les trois

Process Memory Isolation Demo

System Memory

Processus : le "conteneur" d'isolation des ressources

Caractéristiques principales :

  • Forte isolation : chaque processus possède un espace d'adressage virtuel indépendant
  • Surcoût élevé : la création/commutation nécessite l'intervention du système d'exploitation, prend environ 1-10 ms
  • Communication complexe : la communication inter-processus (IPC) nécessite des mécanismes spéciaux (pipes, files de messages, mémoire partagée, etc.)

Scénarios adaptés :

  • Services nécessitant une forte isolation (comme les onglets de navigateur, les programmes sandbox)
  • Services déployés avec un mélange de langages
  • Unités de service nécessitant un redémarrage/mise à jour indépendant

Thread : la "cavalerie légère" à mémoire partagée

Thread Scheduling Demo

Timeline
0ms
100ms
200ms
300ms
400ms
500ms
0
Completed Threads
0
Context Switches
0ms
Avg Wait Time
0
Throughput (threads/s)
Current Scheduling Algorithm: Round Robin (Time Slice)

Each thread takes turns executing for a time slice. When the slice expires, it switches to the next thread. Good responsiveness, suitable for interactive systems.

Caractéristiques principales :

  • Mémoire partagée : les threads d'un même processus partagent le segment de code, le segment de données, le tas
  • Espace de pile indépendant : chaque thread possède sa propre pile (généralement environ 1 Mo)
  • Commutation relativement rapide : la commutation de thread prend environ 1-10 μs, soit 1000 fois plus rapide que le processus
  • Synchronisation nécessaire : les données partagées nécessitent une protection par verrou

Scénarios adaptés :

  • Tâches intensives en CPU (calcul, traitement d'images)
  • Tâches concurrentes nécessitant le partage de nombreuses données
  • Tâches de fond sensibles à la latence

Coroutine : le "thread vert" en espace utilisateur

Coroutine Lightweight Comparison Demo

1000 coroutines
Thread Model
Memory Usage
1000 MB
Creation Time
100 ms
Context Switch
~1-10 us
VS
Coroutine Model
Memory Usage
2000 MB
Creation Time
10 ms
Context Switch
~100 ns
Saves -100% Memory

Caractéristiques principales :

  • Ordonnancement en espace utilisateur : ordonnancé par le programme/bibliothèque d'exécution, sans passer par le système d'exploitation
  • Extrêmement légère : la pile de coroutine ne fait généralement que quelques Ko, on peut en créer des millions
  • Commutation extrêmement rapide : la commutation de coroutine prend environ 100 ns, soit 100 fois plus rapide que le thread
  • Non préemptive : la coroutine cède volontairement le CPU (multitâche coopératif)

Scénarios adaptés :

  • Services à haute concurrence intensive en E/S (serveurs web, passerelles)
  • Scénarios nécessitant de maintenir un grand nombre de connexions persistantes (messagerie instantanée, serveurs de jeux)
  • Traitement de données en flux, pipelines de traitement

2. Étude de cas : les "douleurs de la concurrence" lors d'une grande promotion e-commerce

2.1 Leçons douloureuses : l'évolution du "mono-machine" au "distribué"

Examinons l'histoire réelle de l'évolution d'un système e-commerce :

Étape 1 : l'ère mono-machine (1000 utilisateurs actifs par jour)

python
# Application Flask simple
from flask import Flask

app = Flask(__name__)

@app.route('/order')
def create_order():
    # Vérifier le stock
    stock = db.query("SELECT stock FROM products WHERE id=1")
    if stock > 0:
        # Déduire le stock
        db.execute("UPDATE products SET stock = stock - 1 WHERE id=1")
        # Créer la commande
        db.execute("INSERT INTO orders ...")
        return "Order created!"
    return "Out of stock!"

# Lancement : flask run

Problèmes :

  • Processus unique, thread unique, ne peut traiter qu'une seule requête à la fois
  • La déduction du stock n'est pas verrouillée, ce qui entraîne des surventes en concurrence
  • Le nombre de connexions à la base de données est limité, le pool de connexions est rapidement épuisé

Étape 2 : l'ère multi-processus (10 000 utilisateurs actifs par jour)

python
# Déploiement multi-processus avec Gunicorn
gunicorn -w 4 -k sync app:app

# 4 processus worker, chaque processus traite les requêtes indépendamment

Nouveaux problèmes :

  • 4 processus vérifient le stock simultanément, tous voient stock=1, tous déduisent avec succès, 3 surventes !
  • Nécessité d'introduire un verrou distribué
python
import redis

# Utilisation du verrou distribué Redis
lock = redis_client.lock("stock_lock", timeout=10)
if lock.acquire():
    try:
        stock = db.query("SELECT stock FROM products WHERE id=1")
        if stock > 0:
            db.execute("UPDATE products SET stock = stock - 1 WHERE id=1")
    finally:
        lock.release()

Étape 3 : l'ère des coroutines (100 000 utilisateurs actifs par jour)

python
# Utilisation de FastAPI + asyncio
from fastapi import FastAPI
import asyncio

app = FastAPI()

async def check_stock(product_id: int) -> int:
    # Requête asynchrone à la base de données, sans blocage
    result = await db.fetch_one(
        "SELECT stock FROM products WHERE id = :id",
        {"id": product_id}
    )
    return result["stock"]

@app.get("/order")
async def create_order(product_id: int):
    # Vérification concurrente du stock et des informations utilisateur
    stock_task = check_stock(product_id)
    user_task = get_user_info(request.user_id)

    stock, user = await asyncio.gather(stock_task, user_task)

    if stock > 0:
        # Déduction asynchrone du stock
        await db.execute(
            "UPDATE products SET stock = stock - 1 WHERE id = :id",
            {"id": product_id}
        )
        return {"status": "success"}

    return {"status": "out_of_stock"}

# Lancement : uvicorn main:app --workers 4
# Chaque worker peut traiter des milliers de coroutines concurrentes

Avantages :

  • Un seul thread peut traiter des milliers de connexions concurrentes
  • Cède le CPU lors des opérations d'E/S, sans bloquer les autres requêtes
  • Empreinte mémoire extrêmement faible, adapté aux scénarios de haute concurrence avec connexions persistantes

2.2 Tableau comparatif de l'évolution du modèle de concurrence

ÉtapeModèle de concurrenceUtilisateurs actifs supportésProblème centralSolution
MonolithiqueProcessus unique, thread unique1KImpossible de traiter en concurrenceIntroduction du multi-processus
Multi-processusMulti-processus synchrone10KConcurrence de données, surventeVerrou distribué
Multi-threadMulti-thread + verrous50KSurcoût de commutation de contexte, interblocagePool de threads, files sans verrou
CoroutineE/S asynchrones100K+Complexité du code, débogage difficileEncapsulation par framework, traçage distribué
HybrideMulti-processus + coroutines1000K+Complexité architecturaleGouvernance de services, élasticité

3. Principes approfondis : fonctionnement des différents modèles de concurrence

3.1 Modèle processus : isolation et communication

Mécanisme d'isolation mémoire

Process Memory Isolation Demo

System Memory

Chaque processus possède un espace d'adressage virtuel indépendant :

Mémoire virtuelle du processus A    Mémoire virtuelle du processus B
+----------------+                  +----------------+
|  Espace noyau  |                  |  Espace noyau  |  <-- Partagé (lecture seule)
|  (partagé)     |                  |  (partagé)     |
+----------------+                  +----------------+
|  Espace pile   |                  |  Espace pile   |  <-- Indépendant
|  (croît vers   |                  |  (croît vers   |
|   le bas)      |                  |   le bas)      |
+----------------+                  +----------------+
|  Espace tas    |                  |  Espace tas    |  <-- Indépendant
|  (croît vers   |                  |  (croît vers   |
|   le haut)     |                  |   le haut)     |
+----------------+                  +----------------+
|  Segment       |                  |  Segment       |  <-- Indépendant
|  données       |                  |  données       |
|  (.bss/.data)  |                  |  (.bss/.data)  |
+----------------+                  +----------------+
|  Segment       |                  |  Segment       |  <-- Indépendant
|  code (.text)  |                  |  code (.text)  |
+----------------+                  +----------------+

Modes de communication inter-processus (IPC)

ModePrincipeVitesseScénario adapté
Pipe (Tube)Tampon noyau, flux unidirectionnelMoyenneCommunication entre processus parent/enfant
File de messagesListe chaînée de messages dans le noyauMoyenneTransmission de messages asynchrones
Mémoire partagéeMapping du même bloc de mémoire physiqueLa plus rapidePartage de grandes quantités de données
SémaphoreCompteur noyau-Synchronisation et exclusion mutuelle
SocketPile de protocoles réseauLenteCommunication inter-machine
SignalInterruption logicielle-Notification d'événements

3.2 Modèle thread : ordonnancement et synchronisation

Principe d'ordonnancement des threads

Thread Scheduling Demo

Timeline
0ms
100ms
200ms
300ms
400ms
500ms
0
Completed Threads
0
Context Switches
0ms
Avg Wait Time
0
Throughput (threads/s)
Current Scheduling Algorithm: Round Robin (Time Slice)

Each thread takes turns executing for a time slice. When the slice expires, it switches to the next thread. Good responsiveness, suitable for interactive systems.

Fonctionnement de base de l'ordonnanceur de threads du système d'exploitation :

File d'attente prête               En cours d'exécution           File d'attente bloquée
+--------+                          +--------+                     +--------+
| Thread B|  <-- Fin du quantum     | Thread A|  <-- Requête E/S   | Thread C|
| Thread D|                         | (actif) |                    | Thread E|
| Thread F|                         +--------+                     | (bloqué)|
+--------+                                                        +--------+
    |                                                                  |
    v                                                                  v
L'ordonnanceur choisit le prochain                         Retour à la file prête
à exécuter selon la priorité                               quand l'E/S est terminée

Mécanismes courants de synchronisation des threads

MécanismePrincipeAvantagesInconvénients
Mutex (Verrou d'exclusion mutuelle)État binaire, accès exclusifImplémentation simplePerformances médiocres en cas de forte compétition
RWLock (Verrou lecture-écriture)Lecture partagée, écriture exclusiveEfficace quand lectures > écrituresImplémentation complexe, risque de famine en écriture
Spinlock (Verrou par attente active)Attente active, ne libère pas le CPUEfficace quand l'attente est courteGaspillage de CPU quand l'attente est longue
Variable de conditionAttente d'une condition spécifiqueÉvite l'attente activeDoit être utilisé avec un verrou
Sémaphore (Semaphore)Compteur contrôlant le nombre d'accèsContrôle le nombre de tâches concurrentesFacile à mal utiliser
Opération atomiqueAtomicité au niveau instruction CPUSans verrou, performance maximaleNe peut opérer que sur des types de données simples
File sans verrouImplémentée par opération CASExcellentes performances en haute concurrenceImplémentation complexe, problème ABA

3.3 Modèle coroutine : ordonnancement en espace utilisateur

Coroutine Lightweight Comparison Demo

1000 coroutines
Thread Model
Memory Usage
1000 MB
Creation Time
100 ms
Context Switch
~1-10 us
VS
Coroutine Model
Memory Usage
2000 MB
Creation Time
10 ms
Context Switch
~100 ns
Saves -100% Memory

Avantages fondamentaux de la coroutine

Multithreading traditionnel      vs        Modèle coroutine

+------------+                             +------------+
|  Thread 1   |                            |  Boucle     |
| (pile 1Mo) |                            |  d'événements|
+------------+                             | (ordonnanceur)|
     |                                      +------------+
     v                                           |
+------------+                                    v
|  Thread 2   |                            +------------+
| (pile 1Mo) |                            |  Coroutine A|
+------------+                             | (pile qq Ko)|
     |                                      +------------+
     v                                           |
+------------+                                    v
|  Thread 3   |                            +------------+
| (pile 1Mo) |                            |  Coroutine B|
+------------+                             | (pile qq Ko)|
                                           +------------+

Surcoût : N Mo                               Surcoût : N Ko
Création : ~10 μs                           Création : ~100 ns
Commutation : ~1 μs                         Commutation : ~100 ns

Mécanisme de fonctionnement d'async/await

async/await Mechanism Demo

Python asyncio Example
import asyncio

async def fetch_data(url):
    # await suspends, yields CPU
    response = await aiohttp.get(url)
    # Continue after I/O completes
    return response.json()

async def main():
    # Concurrent execution
    tasks = [fetch_data(url) for url in urls]
    results = await asyncio.gather(*tasks)
Execution Timeline
0ms
50ms
100ms
150ms
200ms
Event Loop
Scheduling
Task 1
Exec
I/O
Exec
I/O
Exec
I/O
Task 2
Exec
I/O
Exec
I/O
Exec
I/O
Task 3
Exec
I/O
Exec
I/O
Exec
I/O
Task 4
Exec
I/O
Exec
I/O
Exec
I/O
Task 5
Exec
I/O
Exec
I/O
Exec
I/O
Concurrent Tasks
5
Total Time
100ms
I/O Wait Time
60ms
CPU Utilization
40%
python
import asyncio

async def fetch_data(url):
    # Au await, la coroutine se suspend et cède le CPU
    response = await aiohttp.get(url)
    # Une fois l'E/S terminée, la boucle d'événements réveille la coroutine,
    # l'exécution reprend ici
    return response.json()

async def main():
    # Créer 3 tâches coroutines
    tasks = [
        fetch_data("https://api1.example.com"),
        fetch_data("https://api2.example.com"),
        fetch_data("https://api3.example.com")
    ]
    # Exécution concurrente, durée totale ≈ la requête la plus lente
    results = await asyncio.gather(*tasks)
    return results

# Lancer la boucle d'événements
asyncio.run(main())

Flux d'exécution :

Chronologie ---------------------------------------------------------------->

Coroutine A: [Prép. requête]--[await suspendu]=======[Réponse reçue]--[Traitement]
                              |
Coroutine B:                  [Prép. requête]--[await suspendu]=======[Réponse]--[Traitement]
                                                |
Coroutine C:                                    [Prép. requête]--[await suspendu]=======[Réponse]
                                                                 |
                                                                 v
                                                        Toutes les E/S terminées

Légende : [ ] = exécution CPU, === = attente E/S, | = commutation de coroutine

3.4 Boucle d'événements : le "cœur" des coroutines

Event Loop Demo

Call Stack
Stack Empty
Event Loop
Check
1Execute synchronous code in the call stack
2Execute all microtasks
3Render UI (if needed)
4Execute macrotask
Task Queue
Microtask Queue
Queue Empty
Macrotask Queue
Queue Empty

La boucle d'événements est le mécanisme central d'ordonnancement des coroutines :

python
import selectors
import heapq

class EventLoop:
    def __init__(self):
        self.selector = selectors.DefaultSelector()
        self.ready = []  # File d'attente prête
        self.scheduled = []  # File de tâches planifiées
        self.current = None

    def run(self):
        while True:
            # 1. Traiter les tâches planifiées
            now = time.time()
            while self.scheduled and self.scheduled[0][0] <= now:
                _, callback = heapq.heappop(self.scheduled)
                self.ready.append(callback)

            # 2. Attendre les événements E/S
            timeout = 0 if self.ready else 0.1
            events = self.selector.select(timeout)

            for key, mask in events:
                callback = key.data
                self.ready.append(callback)

            # 3. Exécuter les callbacks prêts
            while self.ready:
                callback = self.ready.popleft()
                callback()

3.5 Concurrence vs Parallélisme : ce n'est pas la même chose

Concurrency vs Parallelism Demo

CPU Core (Single Core)
CPU 1
Idle
CPU 2
Idle
CPU 3
Idle
CPU 4
Idle
Task Execution
Task 1
40ms
Task 2
30ms
Task 3
50ms
Task 4
35ms
Concurrency vs Parallelism
🔄
Concurrency
Multiple tasks alternate execution, progressing simultaneously at a macro level
Examples: Single-core CPU multi-threading, coroutine scheduling, async I/O
Parallelism
Multiple tasks execute truly simultaneously
Examples: Multi-core CPU computing, GPU parallel computing, distributed processing
What Conditions Are Needed?
Concurrency: A single-core CPU is sufficient
Parallelism: Requires multi-core CPU or multiple machines
ConceptAnglaisSignificationAnalogieCondition requise
ConcurrenceConcurrencyPlusieurs tâches s'exécutent en alternance, progressent simultanément au niveau macroUne personne prépare plusieurs plats en alternanceUn seul cœur CPU suffit
ParallélismeParallelismPlusieurs tâches s'exécutent véritablement en même tempsPlusieurs personnes préparent différents plats simultanémentPlusieurs cœurs CPU ou plusieurs machines

Illustration :

CPU monocœur - Concurrence (Concurrent)
Temps →  1    2    3    4    5    6    7    8
Tâche A: [Exéc][Exéc]      [Exéc][Exéc]
Tâche B:      [Exéc][Exéc]      [Exéc][Exéc]

Deux tâches s'exécutent en alternance, progressent "simultanément" au niveau macro

========================================

CPU multicœur - Parallélisme (Parallel)
Temps →  1    2    3    4    5    6    7    8
Cœur 1: [Tâche A][Tâche A][Tâche A][Tâche A]
Cœur 2: [Tâche B][Tâche B][Tâche B][Tâche B]

Deux tâches s'exécutent véritablement "en même temps"

========================================

En réalité, c'est souvent : Concurrence + Parallélisme
Temps →  1    2    3    4    5    6    7    8
Cœur 1: [A1][A1][B1][B1][C1][C1][D1][D1]
Cœur 2: [A2][A2][B2][B2][C2][C2][D2][D2]

Plusieurs tâches sont d'abord ordonnancées de manière concurrente sur différents cœurs,
puis exécutées en parallèle sur ces cœurs

4. Pratique : Goroutines Go et threads verts

4.1 La philosophie de concurrence de Go

Go Goroutine & GMP Scheduling Demo

Global Queue (G)3
G1
G2
G3
P (Processors) - 4 Total
P0Running
Local Queue
G4
G5
G6
Bound to M0
P1Idle
Local Queue
G7
G8
G9
P2Idle
Local Queue
G10
G11
G12
P3Idle
Local Queue
-
M (Machine Threads) - 4 Total
M0Running
M1Sleeping
M2Sleeping
M3Sleeping

La philosophie de conception de la concurrence en Go : ne pas communiquer en partageant la mémoire, mais partager la mémoire en communiquant.

go
package main

import (
    "fmt"
    "time"
)

// Producteur
func producer(ch chan<- int, id int) {
    for i := 0; i < 5; i++ {
        fmt.Printf("Producer %d sending: %d\n", id, i)
        ch <- i  // Envoyer des données au channel
        time.Sleep(100 * time.Millisecond)
    }
}

// Consommateur
func consumer(ch <-chan int, id int) {
    for val := range ch {  // Recevoir des données du channel
        fmt.Printf("Consumer %d received: %d\n", id, val)
    }
}

func main() {
    // Créer un channel avec buffer
    ch := make(chan int, 10)

    // Lancer 2 goroutines productrices
    for i := 0; i < 2; i++ {
        go producer(ch, i)
    }

    // Lancer 2 goroutines consommatrices
    for i := 0; i < 2; i++ {
        go consumer(ch, i)
    }

    // Attendre un moment
    time.Sleep(3 * time.Second)
    close(ch)
}

4.2 Ordonnanceur de Goroutines : le modèle GMP

L'ordonnanceur de Go adopte le modèle GMP :

ComposantSignificationRôle
G (Goroutine)CoroutineTâche à exécuter, légère (pile de 2 Ko, extensible dynamiquement)
M (Machine)Thread systèmeSupport d'exécution réel de G, correspondance 1:1 avec le thread noyau
P (Processor)Processeur logiqueContexte d'ordonnancement, contient la file de G exécutables, nombre par défaut égal au nombre de cœurs CPU

Flux d'ordonnancement :

File globale
+----------------+
|  G1  |  G2  |  G3  |
+----------------+

File locale de P0       File locale de P1       File locale de P2       File locale de P3
+----------+            +----------+            +----------+            +----------+
| G4 | G5  |            | G6 | G7  |            | G8 | G9  |            | G10| G11 |
+----------+            +----------+            +----------+            +----------+
    |                       |                       |                       |
    v                       v                       v                       v
+----------+            +----------+            +----------+            +----------+
|    M0    |            |    M1    |            |    M2    |            |    M3    |
| (Thread  |            | (Thread  |            | (Thread  |            | (Thread  |
|   OS)    |            |   OS)    |            |   OS)    |            |   OS)    |
+----------+            +----------+            +----------+            +----------+

Stratégie d'ordonnancement :
1. Chaque P maintient une file locale de G, réduisant la compétition de verrous
2. P prend G dans la file locale et le confie à M pour exécution
3. Quand la file locale est vide, "vole" la moitié des G d'un autre P (Work Stealing)
4. La file globale sert de secours, vérifiée périodiquement

5. Templates de code pratiques

5.1 Template Python asyncio pour haute concurrence

python
import asyncio
import aiohttp
from typing import List, Dict
import time

class AsyncHTTPClient:
    """Client HTTP haute performance basé sur asyncio"""

    def __init__(self, max_connections: int = 100, timeout: int = 30):
        self.timeout = aiohttp.ClientTimeout(total=timeout)
        # Limiter le nombre de connexions concurrentes pour éviter de surcharger le service cible
        connector = aiohttp.TCPConnector(
            limit=max_connections,
            limit_per_host=10,  # Limite de connexions par domaine
            enable_cleanup_closed=True,
            force_close=True,
        )
        self.session = aiohttp.ClientSession(
            connector=connector,
            timeout=self.timeout,
        )

    async def fetch(self, url: str, method: str = 'GET', **kwargs) -> Dict:
        """Envoyer une requête unique"""
        try:
            async with self.session.request(method, url, **kwargs) as response:
                return {
                    'url': url,
                    'status': response.status,
                    'data': await response.text(),
                    'error': None
                }
        except asyncio.TimeoutError:
            return {'url': url, 'status': None, 'data': None, 'error': 'Timeout'}
        except Exception as e:
            return {'url': url, 'status': None, 'data': None, 'error': str(e)}

    async def fetch_many(self, urls: List[str], concurrency: int = 10) -> List[Dict]:
        """Récupérer plusieurs URLs en concurrence, avec limite de concurrence"""
        semaphore = asyncio.Semaphore(concurrency)

        async def fetch_with_limit(url):
            async with semaphore:
                return await self.fetch(url)

        # Exécuter toutes les requêtes en concurrence
        tasks = [fetch_with_limit(url) for url in urls]
        return await asyncio.gather(*tasks, return_exceptions=True)

    async def close(self):
        await self.session.close()


# Exemple d'utilisation
async def main():
    client = AsyncHTTPClient(max_connections=50)

    # Liste d'URLs à récupérer
    urls = [
        "https://api.github.com/users/github",
        "https://api.github.com/users/google",
        "https://api.github.com/users/microsoft",
        # ... plus d'URLs
    ] * 10  # Simuler 300 requêtes

    start = time.time()
    results = await client.fetch_many(urls, concurrency=20)
    elapsed = time.time() - start

    # Statistiques
    success = sum(1 for r in results if r.get('status') == 200)
    failed = len(results) - success

    print(f"Total requêtes : {len(results)}")
    print(f"Succès : {success}, Échecs : {failed}")
    print(f"Durée : {elapsed:.2f}s")
    print(f"QPS : {len(results)/elapsed:.1f}")

    await client.close()

if __name__ == "__main__":
    asyncio.run(main())

5.2 Template Go pour service haute concurrence

go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"runtime"
	"time"

	"golang.org/x/sync/errgroup"
)

// Structures Request/Response
type OrderRequest struct {
	UserID    int64   `json:"user_id"`
	ProductID int64   `json:"product_id"`
	Quantity  int     `json:"quantity"`
	Price     float64 `json:"price"`
}

type OrderResponse struct {
	OrderID   int64   `json:"order_id"`
	Status    string  `json:"status"`
	Total     float64 `json:"total"`
	CreatedAt string  `json:"created_at"`
}

// Simulation d'opération base de données
type Database struct {
	orders map[int64]*OrderResponse
	mutex  chan struct{}
}

func NewDatabase() *Database {
	db := &Database{
		orders: make(map[int64]*OrderResponse),
		mutex:  make(chan struct{}, 1), // Simuler un mutex
	}
	return db
}

func (db *Database) CreateOrder(ctx context.Context, req *OrderRequest) (*OrderResponse, error) {
	// Acquérir le verrou
	select {
	case db.mutex <- struct{}{}:
		defer func() { <-db.mutex }()
	case <-ctx.Done():
		return nil, ctx.Err()
	}

	// Simuler la latence de l'opération base de données
	select {
	case <-time.After(50 * time.Millisecond):
	case <-ctx.Done():
		return nil, ctx.Err()
	}

	order := &OrderResponse{
		OrderID:   time.Now().UnixNano(),
		Status:    "created",
		Total:     req.Price * float64(req.Quantity),
		CreatedAt: time.Now().Format(time.RFC3339),
	}
	db.orders[order.OrderID] = order
	return order, nil
}

// Handler HTTP
type Handler struct {
	db *Database
}

func NewHandler(db *Database) *Handler {
	return &Handler{db: db}
}

func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
	// Définir le timeout de la requête
	ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
	defer cancel()

	var req OrderRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	order, err := h.db.CreateOrder(ctx, &req)
	if err != nil {
		if err == context.DeadlineExceeded {
			http.Error(w, "Request timeout", http.StatusGatewayTimeout)
			return
		}
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(order)
}

func (h *Handler) Health(w http.ResponseWriter, r *http.Request) {
	info := map[string]interface{}{
		"status":    "ok",
		"goroutine": runtime.NumGoroutine(),
		"cpu":       runtime.NumCPU(),
		"version":   runtime.Version(),
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(info)
}

// Exemple de traitement par lots
func BatchProcess(ctx context.Context, items []int) ([]int, error) {
	g, ctx := errgroup.WithContext(ctx)
	g.SetLimit(10) // Limiter la concurrence à 10

	results := make([]int, len(items))

	for i, item := range items {
		i, item := i, item // Éviter le piège de closure
		g.Go(func() error {
			select {
			case <-ctx.Done():
				return ctx.Err()
			default:
				// Simuler un traitement
				time.Sleep(100 * time.Millisecond)
				results[i] = item * 2
				return nil
			}
		})
	}

	if err := g.Wait(); err != nil {
		return nil, err
	}
	return results, nil
}

func main() {
	// Initialiser la base de données
	db := NewDatabase()

	// Créer le handler
	handler := NewHandler(db)

	// Configurer les routes
	mux := http.NewServeMux()
	mux.HandleFunc("/order", handler.CreateOrder)
	mux.HandleFunc("/health", handler.Health)

	// Créer le serveur
	server := &http.Server{
		Addr:         ":8080",
		Handler:      mux,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  120 * time.Second,
	}

	fmt.Println("Server starting on :8080")
	fmt.Printf("Go version: %s\n", runtime.Version())
	fmt.Printf("CPU cores: %d\n", runtime.NumCPU())

	if err := server.ListenAndServe(); err != nil {
		log.Fatal(err)
	}
}

6. Tableau récapitulatif comparatif

6.1 Comparaison des concepts fondamentaux

CaractéristiqueProcessusThreadCoroutine
OrdonnanceurSystème d'exploitationSystème d'exploitationProgramme utilisateur / runtime
Surcoût de commutation~1-10 ms~1-10 μs~100 ns
Empreinte mémoire~10 Mo+~1 Mo~2 Ko
Mode de communicationIPCMémoire partagéeMémoire partagée / Channel
Besoin de synchronisationNon nécessaireVerrou nécessaireVerrou nécessaire / coopératif
Impact d'un plantageProcessus concerné uniquementTout le processusContrôlable
Scénario adaptéForte isolation, multi-tenantIntensif CPUIntensif E/S
Langages typiquesTous les langagesTous les langagesGo, Python, JS, Rust

6.2 Guide de choix du modèle de concurrence

ScénarioModèle recommandéRaison
Passerelle de services webCoroutine + E/S asynchronesHaute concurrence de connexions, faible empreinte mémoire
Service de communication temps réelCoroutine + connexions persistantesMaintien de nombreuses connexions WebSocket
Pipeline de traitement de donnéesMulti-processus + coroutinesExploitation multicœur, E/S non bloquantes
Calcul scientifiqueMulti-thread / multi-processusIntensif CPU, nécessite le calcul parallèle
Architecture microservicesMulti-processus + coroutinesIsolation entre services, haute concurrence interne
Systèmes embarquésCoroutine / thread uniqueRessources limitées, ordonnancement déterministe

6.3 Tableau des correspondances terminologiques

Terme anglaisCorrespondance chinoiseExplication
Process进程Unité de base d'allocation des ressources du système d'exploitation, espace mémoire indépendant
Thread线程Unité de base d'ordonnancement du CPU, partage l'espace mémoire du processus
Coroutine协程Thread léger en espace utilisateur, ordonnancé par le programme
Concurrency并发Plusieurs tâches exécutées en alternance, progressent simultanément au niveau macro
Parallelism并行Plusieurs tâches véritablement exécutées simultanément, nécessite le support multicœur
Context Switch上下文切换Processus de passage du CPU d'une tâche à une autre
Blocking I/O阻塞 I/OLe thread est suspendu en attendant la fin de la requête E/S
Non-blocking I/O非阻塞 I/ORetour immédiat après la requête E/S, sans attendre le résultat
Async I/O异步 I/OL'achèvement de l'E/S est notifié à l'appelant par callback ou mécanisme de notification
Event Loop事件循环Mécanisme d'ordonnancement des coroutines, écoute et distribue continuellement les événements
GoroutineGo 协程Implémentation de thread léger en Go
Channel通道Mécanisme de communication entre coroutines en Go
Mutex互斥锁Primitive de synchronisation pour protéger les ressources partagées
Semaphore信号量Contrôle le nombre de threads accédant simultanément à une ressource
Deadlock死锁Plusieurs threads attendent mutuellement la libération de ressources, provoquant un blocage permanent
Race Condition竞态条件Plusieurs threads accèdent simultanément aux données partagées, rendant le résultat indéterminé
Thread Pool线程池Groupe de threads pré-créés et réutilisés pour réduire le surcoût de création/destruction
Work Stealing工作窃取Un thread inactif "vole" des tâches dans la file d'un thread occupé pour les exécuter
Zero-copy零拷贝Transfert de données entre espace noyau et espace utilisateur sans copie CPU
C10K ProblemC10K 问题Défi de traiter 10 000 connexions simultanément sur une seule machine
C10M ProblemC10M 问题Défi ultime de traiter 10 millions de connexions simultanément sur une seule machine

7. En conclusion

7.1 Les règles d'or de la programmation concurrente

  1. Ne pas optimiser prématurément : faites d'abord fonctionner le code correctement, puis envisagez l'optimisation des performances
  2. Éviter l'état partagé : "ne pas communiquer en partageant la mémoire, mais partager la mémoire en communiquant"
  3. Exposer les erreurs le plus tôt possible : les bugs de concurrence sont souvent difficiles à reproduire, il faut les exposer autant que possible lors de la phase de test
  4. Limiter le nombre de tâches concurrentes : la concurrence illimitée équivaut à l'absence de protection, utilisez des sémaphores ou des pools de connexions
  5. Surveillance et observabilité : un système concurrent doit avoir une surveillance complète pour localiser rapidement les problèmes

7.2 Feuille de route d'apprentissage

Étape 1 : Compréhension fondamentale
    ├── Comprendre les concepts de base processus/thread
    ├── Apprendre les primitives de synchronisation (verrous, sémaphores, variables de condition)
    └── Écrire des programmes multithread simples

Étape 2 : Approfondissement des principes
    ├── Comprendre le modèle mémoire et la visibilité
    ├── Apprendre la programmation sans verrou et les opérations atomiques
    ├── Comprendre les pools de threads et le work stealing
    └── Analyser les interblocages et les conditions de concurrence

Étape 3 : Applications avancées
    ├── Maîtriser les coroutines et la programmation asynchrone
    ├── Apprendre les modèles de concurrence de Go/Python/Rust
    ├── Comprendre la concurrence dans les systèmes distribués
    └── Optimisation des performances et planification de capacité

Étape 4 : Niveau expert
    ├── Concevoir des architectures de systèmes hautement concurrents
    ├── Résoudre des bugs de concurrence complexes
    ├── Développer des frameworks de programmation concurrente
    └── Partager et diffuser les connaissances sur la concurrence

Nous espérons que ce guide vous aidera à construire une compréhension systématique de la programmation concurrente. Rappelez-vous, la concurrence n'est pas une fin, mais un moyen — le véritable objectif est de construire des services performants et hautement disponibles. Comprenez les principes, choisissez le bon modèle, écrivez du bon code, et vous irez loin sur le chemin de la concurrence.