← Tous les cours / L'Event Loop & l'Async Cours
00 — L'EVENT LOOP

L'event loop — un thread qui tourne en boucle

Call stack, event queue, microtask queue — comment un seul thread donne l'illusion de faire tout en même temps.

Imagine un cuisinier seul en cuisine. Il ne peut faire qu'une chose à la fois, mais il lance le four, met une casserole à chauffer, puis coupe des légumes pendant que les deux cuissent. Il n'est pas en train de faire trois choses simultanément — il délègue l'attente et revient consulter quand c'est prêt.

L'event loop fonctionne exactement ainsi. Un seul thread exécute le code JavaScript — jamais deux instructions en même temps. Mais il peut lancer des opérations longues (réseau, timers, I/O) et continuer à traiter d'autres tâches pendant qu'elles s'exécutent ailleurs.

Les quatre zones de l'event loop
Call Stack : la pile d'appels de fonctions en cours. LIFO.
Heap : zone mémoire où vivent les objets alloués.
Task Queue (macrotask queue) : callbacks en attente — setTimeout, setInterval, événements I/O.
Microtask Queue : callbacks de haute priorité — Promise.then, queueMicrotask. Vidée entièrement avant la prochaine macrotask.

La boucle elle-même est simple :

// Pseudo-code de l'event loop
while (true) {
    // 1. Exécute tout le code synchrone (vide la call stack)
    executeSynchronousCode();

    // 2. Vide entièrement la microtask queue
    while (microtaskQueue.length > 0) {
        microtaskQueue.shift()();
    }

    // 3. Prend UNE macrotask dans la task queue
    if (taskQueue.length > 0) {
        taskQueue.shift()();
    }

    // 4. Recommence
}
💡 L'event loop ne traite qu'UNE macrotask à la fois, puis vide toute la microtask queue avant d'en prendre une autre. Ce détail explique pourquoi Promise.then s'exécute avant setTimeout(fn, 0) même si le setTimeout a été enregistré en premier.

Lance la simulation pour observer comment A, setTimeout, Promise.then et D traversent les queues.

CALL STACK
MICROTASK QUEUE
TASK QUEUE
SORTIE CONSOLE
01 — CALL STACK

La call stack — LIFO des appels de fonctions

Chaque appel de fonction empile un frame. LIFO. Une exception remonte la pile. Toujours synchrone.

La call stack est une pile : le dernier arrivé est le premier sorti (LIFO — Last In, First Out). À chaque appel de fonction, un frame est empilé. Quand la fonction retourne, son frame est dépilé. C'est le mécanisme de base de tout langage à thread unique.

Frame de la call stack
Chaque frame contient : la référence à la fonction, ses arguments, ses variables locales, et le pointeur de retour (là où l'exécution reprend après le retour).

Voici ce qui se passe quand tu appelles trois fonctions imbriquées :

function c() { return 42; }
function b() { return c(); }
function a() { return b(); }

a(); // appel initial

La stack évolue ainsi :

a() appelée [ a ]
b() appelée depuis a [ b ] [ a ]
c() appelée depuis b [ c ] [ b ] [ a ]
retours en cascade [ vide ]

Stack overflow

La call stack a une taille limitée (environ 10 000 frames en V8). Une récursion infinie la remplit jusqu'à l'erreur Maximum call stack size exceeded.

function infini() {
    return infini(); // aucune condition d'arrêt
}
infini(); // RangeError: Maximum call stack size exceeded
⚠️ Stack trace d'une exception : quand une erreur est levée, JavaScript remonte la call stack pour afficher la trace. C'est pourquoi Error.stack montre l'ordre LIFO des appels — le dernier appel (le plus imbriqué) est en haut.

La stack est toujours synchrone

Un point crucial : la call stack ne peut jamais être interrompue. Une fonction asynchrone ne "met pas en pause" la stack — elle retourne immédiatement et enregistre un callback quelque part. Le callback sera exécuté plus tard, quand la stack sera vide.

console.log('début');

setTimeout(function() {
    console.log('timer'); // exécuté APRÈS que la stack soit vide
}, 0);

console.log('fin');

// Sortie : début → fin → timer
💡 Tail call optimization (TCO) : ES2015 spécifie que les appels en position terminale (tail position) peuvent réutiliser le frame courant au lieu d'en créer un nouveau, évitant le stack overflow. En pratique, seul Safari l'implémente complètement à ce jour.
02 — MACRO VS MICROTASKS

Macrotasks et Microtasks — ordre d'exécution

Les microtasks (Promise.then) passent toujours avant la prochaine macrotask (setTimeout) — une règle à connaître par cœur.

L'event loop gère deux types de files d'attente avec des priorités différentes. Comprendre la différence entre macrotasks et microtasks est l'une des règles les plus importantes pour écrire du code async correct.

Macrotask (Task Queue)
Callbacks enregistrés par : setTimeout, setInterval, setImmediate (Node.js), événements DOM, requêtes XHR terminées, I/O complété.
L'event loop en traite une seule par tour, puis vide les microtasks.
Microtask (Microtask Queue)
Callbacks enregistrés par : Promise.then/catch/finally, queueMicrotask(), MutationObserver (navigateur).
Toute la queue est vidée avant la prochaine macrotask, sans exception. Même si de nouvelles microtasks sont ajoutées pendant la vidange.

La règle mnémotechnique : micro avant macro. Après chaque bout de code synchrone, toutes les microtasks s'exécutent. Puis seulement la prochaine macrotask.

console.log('1 — synchrone');

setTimeout(function() {
    console.log('4 — macrotask');
}, 0);

Promise.resolve()
    .then(function() { console.log('3 — microtask'); });

queueMicrotask(function() {
    console.log('encore 3 — microtask aussi');
});

console.log('2 — synchrone');

// Sortie : 1 → 2 → 3 → encore 3 → 4
⚠️ Microtask starvation : si une microtask ajoute une nouvelle microtask (récursivement), la macrotask suivante ne sera jamais exécutée. La boucle de rendu du navigateur (qui est une macrotask) sera bloquée.
// PIÈGE — microtask récursive qui bloque le rendu
function loopMicrotask() {
    Promise.resolve().then(loopMicrotask); // ajoute toujours une nouvelle microtask
}
loopMicrotask(); // le rendu de la page est gelé

Avance pas à pas pour voir l'ordre d'exécution de setTimeout, Promise.then et queueMicrotask.

03 — I/O NON-BLOQUANT

I/O non-bloquant — epoll et le noyau

setTimeout et fetch ne bloquent pas — ils délèguent au kernel qui rappelle quand c'est prêt. C'est ça, l'I/O non-bloquant.

Voici le cœur du mystère : si JavaScript est mono-thread, comment fait-il pour gérer des milliers de connexions simultanées sans se bloquer ? La réponse est dans le noyau du système d'exploitation.

File descriptor (fd)
Un entier que le kernel associe à une ressource I/O ouverte : socket réseau, fichier disque, pipe. Quand tu appelles open(), socket() etc., le kernel retourne un fd. Toutes les opérations (read, write, close) passent par ce fd.

epoll / kqueue / IOCP

Le kernel expose des APIs permettant à un programme de surveiller des milliers de fds sans bloquer :

SystèmeAPIMécanisme
Linux epoll Enregistre les fds à surveiller, epoll_wait() retourne ceux qui sont prêts
macOS / BSD kqueue Similaire, gère aussi les signaux et les timers
Windows IOCP I/O Completion Ports — modèle orienté completion plutôt que readiness

Node.js utilise libuv, une bibliothèque C qui abstrait ces trois mécanismes derrière une interface unifiée.

Ton code JS
libuv
epoll / kqueue / IOCP
Kernel

Le cycle d'une opération réseau

// Ce qui se passe quand tu fais :
fetch('https://api.example.com/data')
    .then(res => res.json())
    .then(data => console.log(data));

// 1. JS crée une socket, l'enregistre dans epoll (non-bloquant)
// 2. JS continue à s'exécuter — call stack libre
// 3. Le kernel fait la connexion TCP + HTTP en arrière-plan
// 4. Quand la réponse arrive : epoll_wait() retourne le fd prêt
// 5. libuv lit la réponse, crée un callback et le pousse dans la Task Queue
// 6. L'event loop traite le callback → .then() s'exécute

Thread pool — pour les opérations qui ne peuvent pas être async

Certaines opérations n'ont pas de syscall async natif. libuv maintient un thread pool (4 threads par défaut, configurable via UV_THREADPOOL_SIZE) pour les traiter sans bloquer l'event loop :

OpérationPourquoi le thread pool
fs.readFile, fs.statOpérations fichier synchrones dans le kernel sur certains OS
dns.lookup()Résolution DNS système (getaddrinfo) est bloquante
crypto (bcrypt, scrypt)CPU-intensif, ne doit pas bloquer l'event loop
zlib (compression)CPU-intensif
💡 I/O réseau ≠ thread pool : les requêtes HTTP, les sockets TCP, les connexions base de données (PostgreSQL) utilisent epoll directement — pas le thread pool. C'est pour ça que Node.js peut gérer des milliers de connexions avec seulement 4 threads de pool.
04 — PROMISES & ASYNC/AWAIT

Promises et async/await

Promise.all, Promise.race, async/await — les primitives du parallélisme coopératif. await suspend la fonction, pas le thread.

Les Promises sont la réponse à l'enfer des callbacks imbriqués. Une Promise représente une valeur qui n'est pas encore disponible mais le sera (ou non) dans le futur.

États d'une Promise
pending : en attente — ni résolue ni rejetée.
fulfilled : résolue avec une valeur.
rejected : rejetée avec une raison (erreur).
Une fois sortie de pending, une Promise est immuable — impossible de changer son état.

Chaining de Promises

Chaque .then() retourne une nouvelle Promise. C'est ce qui permet le chaînage :

fetch('/api/user/1')
    .then(res  => res.json())
    .then(user => fetch(`/api/posts?userId=${user.id}`))
    .then(res  => res.json())
    .then(posts => console.log(posts))
    .catch(err  => console.error(err));

async/await — sucre syntaxique sur les Promises

async/await ne crée pas de nouveau mécanisme — c'est une façon plus lisible d'écrire du code basé sur les Promises. Une fonction async retourne toujours une Promise. await suspend la fonction courante (pas le thread) jusqu'à ce que la Promise soit résolue.

// Équivalent exact du chaînage ci-dessus
async function loadPosts(userId) {
    try {
        const user  = await (await fetch(`/api/user/${userId}`)).json();
        const posts = await (await fetch(`/api/posts?userId=${user.id}`)).json();
        return posts;
    } catch (err) {
        console.error(err);
    }
}
💡 await ne bloque pas le thread : pendant qu'une fonction async est "en attente" sur un await, l'event loop continue à traiter d'autres callbacks. La fonction est simplement suspendue — son contexte est sauvegardé en mémoire.

Les combinateurs de Promises

MéthodeComportementCas d'usage
Promise.all([]) Attend toutes — échoue dès le premier rejet Requêtes parallèles toutes requises
Promise.allSettled([]) Attend toutes — ne rejette jamais, retourne status + value/reason Audit, rapports — on veut tous les résultats même partiels
Promise.race([]) Retourne dès la première résolue ou rejetée Timeout, fallback sur le plus rapide
Promise.any([]) Retourne dès la première résolue (ignore les rejets) Plusieurs endpoints alternatifs — premier disponible gagne
// Parallélisme : les trois fetch lancés simultanément
const [users, products, orders] = await Promise.all([
    fetch('/api/users')    .then(r => r.json()),
    fetch('/api/products') .then(r => r.json()),
    fetch('/api/orders')   .then(r => r.json()),
]);

// Timeout : abort si plus de 3 secondes
const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('timeout')), 3000)
);
const result = await Promise.race([fetch('/api/data'), timeout]);
05 — NODE.JS — 6 PHASES

Node.js — les 6 phases de l'event loop

timers → pending callbacks → poll → check → close — l'ordre précis qui explique pourquoi setImmediate est différent de setTimeout(0).

Node.js utilise une implémentation de l'event loop plus détaillée que le navigateur. libuv décompose le tour de boucle en 6 phases, chacune traitant un type de callbacks spécifique.

Les 6 phases de libuv
1. timers : callbacks setTimeout et setInterval dont le délai est écoulé.
2. pending callbacks : callbacks d'erreurs I/O reportés du tour précédent.
3. idle, prepare : usage interne libuv.
4. poll : récupère les nouveaux événements I/O ; s'il n'y a rien, attend.
5. check : callbacks setImmediate().
6. close callbacks : événements close (ex. socket.destroy()).
timers
pending
idle
poll
check
close

Entre chaque transition de phase, Node.js vide la microtask queue (Promises) et traite process.nextTick().

process.nextTick() — priorité maximale

process.nextTick() n'est pas techniquement une phase de l'event loop. Son callback est exécuté immédiatement après le code synchrone courant, avant tout le reste — même avant les microtasks Promise.

console.log('A');

process.nextTick(() => console.log('B — nextTick'));

Promise.resolve().then(() => console.log('C — Promise'));

setTimeout(() => console.log('D — setTimeout'), 0);

setImmediate(() => console.log('E — setImmediate'));

console.log('F');

// Sortie : A → F → B → C → D → E
// (D et E peuvent être inversés hors I/O — dépend du timing de l'OS)

setImmediate() vs setTimeout(fn, 0)

setImmediatesetTimeout(fn, 0)
Phase check (phase 5) timers (phase 1)
Délai minimum Après la phase poll du tour courant ≥ 1ms (norme HTML), variable
Dans un callback I/O Toujours avant setTimeout Toujours après setImmediate
En dehors d'un callback I/O Ordre non déterminé Ordre non déterminé
💡 Règle pratique : utilise setImmediate() quand tu veux que quelque chose s'exécute après l'I/O courant, sans délai supplémentaire. C'est plus prédictible que setTimeout(fn, 0) dans un contexte serveur.
⚠️ process.nextTick avec récursion : comme les microtasks, un process.nextTick récursif peut affamer l'event loop. Node.js ne passe jamais à la phase suivante si la queue nextTick n'est pas vide.
06 — PHP ASYNC

PHP — Fibers, Swoole et FrankenPHP

PHP traditionnel = synchrone. PHP 8.1 Fibers + ReactPHP/AMPHP = concurrence coopérative. FrankenPHP = workers Go.

PHP a longtemps été exclusivement synchrone. Un script s'exécute de haut en bas, une requête par processus (FPM). Depuis PHP 8.1, l'écosystème a profondément évolué.

PHP-FPM — modèle classique
Chaque requête HTTP obtient un processus PHP dédié. Si la requête fait une requête SQL de 200ms, le processus est bloqué 200ms. Pour 1000 requêtes simultanées, il faut 1000 processus.

PHP Fibers (8.1+) — coroutines natives

Les Fibers sont les coroutines de PHP. Un Fiber peut être suspendu (Fiber::suspend()) et repris (Fiber::resume()) sans bloquer le thread. C'est la primitive sur laquelle AMPHP v3 est construit.

$fiber = new Fiber(function (): void {
    $value = Fiber::suspend('premier'); // rend la main, retourne 'premier'
    echo "repris avec : $value\n";      // exécuté après resume()
});

$yielded = $fiber->start();            // lance, reçoit 'premier'
echo "suspendu avec : $yielded\n";    // suspendu avec : premier
$fiber->resume('bonjour');            // repris avec : bonjour

Swoole — extension C avec event loop propre

Swoole est une extension PHP en C qui remplace FPM par un serveur d'application avec sa propre event loop, ses coroutines et son thread pool. Code PHP standard, mais chaque requête s'exécute dans une coroutine.

use Swoole\Coroutine;

// Deux requêtes HTTP en parallèle dans une coroutine Swoole
Coroutine\run(function () {
    [$r1, $r2] = Coroutine::map([
        fn() => Coroutine\Http\Client::get('https://api.example.com/a'),
        fn() => Coroutine\Http\Client::get('https://api.example.com/b'),
    ]);
    // r1 et r2 sont arrivés en parallèle
});

ReactPHP — event loop pure PHP

ReactPHP implémente une event loop en PHP pur (basée sur stream_select ou libevent). Tout est géré par callbacks/Promises, sans extension C.

$loop = React\EventLoop\Factory::create();
$browser = new React\Http\Browser($loop);

$browser->get('https://httpbin.org/get')
    ->then(function ($response) {
        echo $response->getBody();
    });

$loop->run(); // démarre la boucle

FrankenPHP — le serveur de ce projet

FrankenPHP est un serveur PHP écrit en Go qui intègre PHP directement dans le runtime Go. En mode worker, un processus PHP reste actif entre les requêtes (comme Swoole), éliminant le coût de démarrage. Les requêtes sont dispatchées par les goroutines Go.

Mode worker FrankenPHP
Le worker PHP tourne en boucle : il traite une requête, puis appelle frankenphp_handle_request() pour attendre la suivante. Le contexte PHP (classes, connexions DB) est réutilisé — démarrage en millisecondes au lieu de millisecondes à chaque requête.
💡 Ce projet utilise FrankenPHP en mode worker Symfony. La concurrence est gérée par Go (goroutines), pas par une event loop PHP. Les requêtes PHP restent synchrones — c'est Go qui parallélise.
07 — BLOQUER L'EVENT LOOP

Bloquer l'event loop — le piège classique

Un JSON.parse() de 500ms fige l'event loop. Toutes les requêtes attendent. Un seul appel lent peut mettre à genoux ton serveur Node.js.

C'est le piège le plus dangereux de Node.js et de tout système basé sur une event loop : une seule opération synchrone longue bloque TOUTES les requêtes simultanées.

⚠️ Le thread est partagé entre toutes les requêtes. Si tu bloques 500ms, toutes les connexions actives attendent 500ms. Un seul utilisateur qui envoie un gros JSON peut dégrader l'expérience de tous les autres.

Exemples d'opérations qui bloquent

// ❌ JSON.parse sur un fichier de 50 MB
const data = JSON.parse(fs.readFileSync('big.json', 'utf8'));

// ❌ Boucle CPU intensive
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}
fibonacci(45); // ~5 secondes — event loop gelée

// ❌ Regex catastrophique (ReDoS)
const evil = /(a+)+b/;
evil.test('aaaaaaaaaaaaaaaaaaaaac'); // temps exponentiel

// ❌ Synchronous crypto
const hash = crypto.scryptSync(password, salt, 64); // peut prendre 100-200ms

Mesurer le blocage

// Détecte quand l'event loop est bloquée plus de 50ms
let lastTick = Date.now();
setInterval(function() {
    const delta = Date.now() - lastTick;
    if (delta > 50) console.warn(`Event loop lag: ${delta}ms`);
    lastTick = Date.now();
}, 10);

Solutions

ProblèmeSolution
Calcul CPU long (crypto, image, compression) Worker threads (worker_threads Node.js)
Rendu lourd dans le navigateur Web Workers, requestIdleCallback
Gros JSON à parser Streaming JSON parser, web worker
Algorithme récursif lent Découpage en chunks via setTimeout(fn, 0)
ReDoS (regex catastrophique) Valider les regexes, limiter la taille des inputs
// Découpage d'une boucle longue en microtâches
async function processLargeArray(items) {
    const CHUNK_SIZE = 1000;
    for (let i = 0; i < items.length; i += CHUNK_SIZE) {
        items.slice(i, i + CHUNK_SIZE).forEach(process);
        // cède la main à l'event loop entre chaque chunk
        await new Promise(resolve => setTimeout(resolve, 0));
    }
}
08 — CONCURRENCE VS PARALLÉLISME

Concurrence vs parallélisme

L'event loop est concurrente (un thread, plusieurs tâches en vol). Le thread pool est parallèle (plusieurs threads). Deux mécanismes qui ne font pas la même chose.

Ces deux termes sont souvent confondus. Ils décrivent des mécanismes fondamentalement différents, et choisir le bon a des conséquences importantes sur l'architecture.

Concurrence
Plusieurs tâches progressent sur un seul thread en alternant leur exécution. À tout instant, une seule tâche s'exécute réellement. Le thread passe d'une tâche à l'autre quand l'une est en attente (I/O, timer).
→ C'est le modèle de l'event loop.
Parallélisme
Plusieurs tâches s'exécutent simultanément sur plusieurs cœurs physiques. Deux threads s'exécutent au même instant sur deux cœurs différents.
→ C'est le modèle du thread pool.
Concurrence (event loop)

Un chef orchestre plusieurs tâches. Quand une tâche attend (I/O), il passe à la suivante. Efficace pour le travail I/O-bound.

Thread: ─A─A─[wait]─B─B─A─A─B─

Toujours un seul thread. Coordination sans locks. Idéal : APIs, proxies, chat.

Parallélisme (thread pool)

Plusieurs ouvriers travaillent simultanément. Chacun fait une tâche indépendante. Nécessaire pour le CPU-bound.

Thread 1: ─A─A─A─A─A─A─A─
Thread 2: ─B─B─B─B─B─B─B─

Plusieurs threads. Nécessite des locks/atomics. Idéal : encodage vidéo, crypto, ML.

Choisir le bon mécanisme

Type de travailMécanismePourquoi
Attente I/O (réseau, DB, fichier) Event loop Le thread n'est pas consommé pendant l'attente
Calcul CPU (encodage, chiffrement) Thread pool / Worker Libère l'event loop, utilise les cœurs disponibles
Traitement parallèle de données Worker threads (Node.js) Isolation mémoire, communication par messages
Rendu UI non-bloquant (navigateur) Web Workers Le main thread reste réactif aux événements UI
💡 La concurrence n'est pas le parallélisme. Un serveur Node.js avec une event loop peut gérer 50 000 connexions simultanées — en concurrence sur un thread. Ajouter des Worker Threads n'améliore pas les performances I/O : ça ajoute du parallélisme pour les tâches CPU.

Back-pressure

Si les callbacks arrivent plus vite qu'ils ne sont traités, la Task Queue grossit — et la mémoire avec elle. C'est la back-pressure : la source produit plus vite que le consommateur n'absorbe.

// Streams Node.js avec back-pressure explicite
const readable = fs.createReadStream('big.log');
const writable = fs.createWriteStream('filtered.log');

// pipe() gère la back-pressure automatiquement
// il pause le readable quand le writable est saturé
readable.pipe(writable);
09 — SYNTHÈSE

Synthèse — quand utiliser l'async

I/O-bound vs CPU-bound, workers vs event loop, async PHP quand ça vaut vraiment le coup.

Un tour d'horizon complet de l'event loop. Il est temps de fixer les règles de décision : quand utiliser l'async, quand éviter, et comment diagnostiquer les problèmes.

I/O-bound vs CPU-bound

I/O-bound
Le goulot d'étranglement est l'attente d'une ressource externe : réseau, base de données, système de fichiers. Le thread est libre pendant l'attente. L'event loop est parfaite ici — elle réutilise ce temps pour d'autres tâches.
CPU-bound
Le goulot est le calcul pur : chiffrement, compression, traitement d'image, algorithmes lourds. Le thread est occupé à 100%, il ne peut pas "attendre". L'event loop est impuissante — il faut du parallélisme (Worker threads).
SituationRecommandationOutil
API qui fait des requêtes DB Event loop (I/O-bound) async/await Node.js, Promises
Proxy HTTP / WebSocket Event loop — excellente scalabilité Node.js natif
Encoding vidéo / image Worker threads (CPU-bound) worker_threads + MessageChannel
Traitement de logs volumineuses Streams Node.js + back-pressure fs.createReadStream + pipe()
PHP — API CRUD classique Synchrone suffit (FPM / FrankenPHP) Symfony + PostgreSQL
PHP — appels HTTP parallèles Fibers + AMPHP ou Guzzle concurrent AMPHP v3, ReactPHP
PHP — tâches CPU lourdes Queue de travaux asynchrones Symfony Messenger, RabbitMQ

Récapitulatif des règles d'exécution

Ordre garanti dans l'event loop JavaScript
1. Code synchrone (call stack vidée entièrement)
2. process.nextTick() (Node.js uniquement — avant les microtasks)
3. Microtask Queue — Promise.then, queueMicrotask() — vidée entièrement
4. Une macrotask (Task Queue) — setTimeout, setInterval, I/O
5. Microtask Queue à nouveau — vidée entièrement
6. Prochaine macrotask → recommence

Diagnostiquer un problème d'event loop

💡 L'async n'est pas une baguette magique : il ne rend pas le code plus rapide — il rend le thread disponible pendant l'attente. Un code CPU-bound en async/await est exactement aussi lent que son équivalent synchrone. Identifier d'abord le goulot (I/O ou CPU) avant de choisir l'outil.

Tu maîtrises maintenant les rouages de l'event loop : la call stack qui empile et dépile, les microtasks qui s'intercalent avant chaque macrotask, le kernel qui gère l'I/O en arrière-plan, et la différence fondamentale entre concurrence et parallélisme. Ces mécanismes sont au cœur de Node.js, des navigateurs modernes, et des implémentations async de PHP.