Preuves empiriques : pourquoi les coroutines monothread fonctionnent

L’affirmation selon laquelle la concurrence cooperative monothread est efficace pour les charges IO-bound est etayee par des mesures, des recherches academiques et l’experience operationnelle de systemes a grande echelle.


1. Cout de la commutation : coroutine vs thread OS

Le principal avantage des coroutines est que la commutation cooperative s’effectue dans l’espace utilisateur, sans invoquer le noyau du systeme d’exploitation.

Mesures sous Linux

Metrique Thread OS (Linux NPTL) Coroutine / tache asynchrone
Changement de contexte 1,2-1,5 us (epingle), ~2,2 us (non epingle) ~170 ns (Go), ~200 ns (Rust async)
Creation de tache ~17 us ~0,3 us
Memoire par tache ~9,5 Kio (min), 8 Mio (pile par defaut) ~0,4 Kio (Rust), 2-4 Kio (Go)
Scalabilite ~80 000 threads (test) 250 000+ taches async (test)

Sources :

Ce que cela signifie en pratique

La commutation d’une coroutine coute ~200 nanosecondes – un ordre de grandeur moins cher que la commutation d’un thread OS (~1,5 us). Mais plus important encore, la commutation de coroutine n’entraine pas de couts indirects : vidage du cache TLB, invalidation du predicteur de branchement, migration entre coeurs – tous ces phenomenes sont caracteristiques des threads, mais pas des coroutines au sein d’un meme thread.

Pour une boucle d’evenements gerant 80 coroutines par coeur, la surcharge totale de commutation est :

80 × 200 ns = 16 us pour un cycle complet a travers toutes les coroutines

C’est negligeable par rapport aux 80 ms de temps d’attente d’E/S.


2. Memoire : echelle des differences

Les threads OS allouent une pile de taille fixe (8 Mio par defaut sous Linux). Les coroutines ne stockent que leur etat – variables locales et point de reprise.

Implementation Memoire par unite de concurrence
Thread Linux (pile par defaut) 8 Mio virtuel, ~10 Kio RSS minimum
Goroutine Go 2-4 Kio (pile dynamique, croit selon les besoins)
Coroutine Kotlin quelques dizaines d’octets sur le tas ; ratio thread:coroutine ≈ 6:1
Tache async Rust ~0,4 Kio
Frame coroutine C++ (Pigweed) 88-408 octets
Coroutine Python asyncio ~2 Kio (vs ~5 Kio + 32 Kio de pile pour un thread)

Sources :

Implications pour les serveurs web

Pour 640 taches concurrentes (8 coeurs × 80 coroutines) :


3. Le probleme C10K et les serveurs reels

Le probleme

En 1999, Dan Kegel a formule le probleme C10K : les serveurs utilisant le modele “un thread par connexion” etaient incapables de servir 10 000 connexions simultanees. La cause n’etait pas les limitations materielles, mais la surcharge des threads OS.

La solution

Le probleme a ete resolu par la transition vers une architecture evenementielle : au lieu de creer un thread pour chaque connexion, une seule boucle d’evenements sert des milliers de connexions dans un seul thread.

C’est exactement l’approche implementee par nginx, Node.js, libuv, et – dans le contexte PHP – True Async.

Benchmarks : nginx (evenementiel) vs Apache (thread par requete)

Metrique (1000 connexions concurrentes) nginx Apache
Requetes par seconde (statique) 2 500-3 000 800-1 200
Debit HTTP/2 >6 000 req/s ~826 req/s
Stabilite sous charge Stable Degradation a >150 connexions

nginx sert 2-4x plus de requetes qu’Apache, tout en consommant significativement moins de memoire. Apache avec son architecture thread-par-requete n’accepte pas plus de 150 connexions simultanees (par defaut), apres quoi les nouveaux clients attendent dans une file.

Sources :


4. Recherche academique

SEDA : Staged Event-Driven Architecture (Welsh et al., 2001)

Matt Welsh, David Culler et Eric Brewer de l’UC Berkeley ont propose SEDA – une architecture serveur basee sur les evenements et les files entre les etapes de traitement.

Resultat cle : le serveur SEDA en Java a surpasse Apache (C, thread-par-connexion) en debit a 10 000+ connexions simultanees. Apache ne pouvait pas accepter plus de 150 connexions simultanees.

Welsh M., Culler D., Brewer E. SEDA: An Architecture for Well-Conditioned, Scalable Internet Services. SOSP ‘01 (2001). PDF

Comparaison des architectures de serveurs web (Pariag et al., 2007)

La comparaison la plus approfondie des architectures a ete menee par Pariag et al. de l’Universite de Waterloo. Ils ont compare trois serveurs sur la meme base de code :

Resultat cle : le userver evenementiel et WatPipe base sur pipeline ont fourni ~18% de debit en plus que le Knot base sur threads. WatPipe necessitait 25 threads d’ecriture pour atteindre les memes performances que userver avec 10 processus.

Pariag D. et al. Comparing the Performance of Web Server Architectures. EuroSys ‘07 (2007). PDF

AEStream : acceleration du traitement evenementiel avec les coroutines (2022)

Une etude publiee sur arXiv a effectue une comparaison directe des coroutines et des threads pour le traitement de flux de donnees (traitement evenementiel).

Resultat cle : les coroutines ont fourni au moins 2x le debit par rapport aux threads conventionnels pour le traitement de flux d’evenements.

Pedersen J.E. et al. AEStream: Accelerated Event-Based Processing with Coroutines. (2022). arXiv:2212.10719


5. Scalabilite : 100 000 taches

Kotlin : 100 000 coroutines en 100 ms

Dans le benchmark TechYourChance, la creation et le lancement de 100 000 coroutines ont necessite ~100 ms de surcharge. Un nombre equivalent de threads necessiterait ~1,7 seconde rien que pour la creation (100 000 × 17 us) et ~950 Mio de memoire pour les piles.

Rust : 250 000 taches async

Dans le benchmark context-switch, 250 000 taches async ont ete lancees dans un seul processus, tandis que les threads OS ont atteint leur limite a ~80 000.

Go : des millions de goroutines

Go lance couramment des centaines de milliers et des millions de goroutines dans les systemes de production. C’est ce qui permet aux serveurs comme Caddy, Traefik et CockroachDB de gerer des dizaines de milliers de connexions simultanees.


6. Resume des preuves

Affirmation Confirmation
La commutation de coroutine est moins couteuse que les threads ~200 ns vs ~1500 ns – 7-8x (Bendersky 2018, Blandy)
Les coroutines consomment moins de memoire 0,4-4 Kio vs 9,5 Kio-8 Mio – 24x+ (Blandy, Go FAQ)
Un serveur evenementiel est plus scalable nginx 2-4x debit vs Apache (benchmarks)
Evenementiel > thread-par-connexion (academiquement) +18% debit (Pariag 2007), C10K resolu (Kegel 1999)
Coroutines > threads pour le traitement d’evenements 2x debit (AEStream 2022)
Centaines de milliers de coroutines dans un processus 250K taches async (Rust), 100K coroutines en 100ms (Kotlin)
La formule N ≈ 1 + T_io/T_cpu est correcte Goetz 2006, Zalando, loi de Little

References

Mesures et benchmarks

Articles academiques

Experience industrielle

Voir aussi