Vol. 7 · № 03 · Un atelier RustA Rust workshop

Les canaux, ou share memory by communicating Channels, or share memory by communicating

Passer des messages plutôt que partager de la mémoire : la possession transférée par le canal. Plus rien à partager, donc plus de course — par construction, pas par verrou. Pass messages rather than share memory: ownership transferred through the channel. Nothing left to share, hence no race — by construction, not by lock.

AudienceAudience
Dev maîtrisant possession, threads et Arc<Mutex> (Vol 1, Vol 7 №1–2) Dev fluent in ownership, threads and Arc<Mutex> (Vol 1, Vol 7 №1–2)
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 19 min ●●●○ ConcurrenceCanauxmpsc
SommaireContents ·
№ 01CadrageFraming3 min

Et si la difficulté venait du partage — qu'on cesserait alors de partager ?What if the difficulty came from sharing — which we'd then stop doing?

Le numéro précédent a rouvert le partage mutable, mais au prix d'un verrou, d'une contention à doser et d'un deadlock qu'aucune preuve ne couvre. C'est le terrain difficile de la concurrence — et il le reste, même en Rust. Les canaux retournent le problème au lieu de l'affronter : plutôt que plusieurs threads se disputant une même mémoire mutable, un thread envoie un message, un autre le reçoit. La devise, empruntée à Go et faite sienne par Rust : « ne communique pas en partageant la mémoire ; partage la mémoire en communiquant ». Et le mécanisme qui rend cela sûr, on le connaît déjà : la possession de la donnée est déplacée à travers le canal — le move du Vol 1. L'émetteur la perd, le récepteur la gagne. À aucun instant deux threads ne la tiennent ensemble. Le data race n'est pas interdit : il devient impossible à exprimer, parce qu'il n'y a plus rien à partager.The previous issue reopened shared mutation, but at the price of a lock, of contention to balance, and of a deadlock no proof covers. That's concurrency's hard ground — and it stays hard, even in Rust. Channels flip the problem instead of fighting it: rather than several threads contending over the same mutable memory, one thread sends a message, another receives it. The motto, borrowed from Go and made Rust's own: 'don't communicate by sharing memory; share memory by communicating'. And the mechanism that makes this safe, we already know: ownership of the data is moved through the channel — Vol 1's move. The sender loses it, the receiver gains it. At no instant do two threads hold it together. The data race isn't forbidden: it becomes inexpressible, because there's nothing left to share.

La possession entre par send(), ressort par recv()Ownership enters via send(), exits via recv()
thread émetteursender thread
message: String
possèdeowns
send(msg) ▸move : entremove: in
canalchannel
≋≋≋
FIFO
▸ recv()move : sortmove: out
thread récepteurreceiver thread
recu: String
possèdeowns
Le canal, en un aller simpleThe channel, in one one-way trip
use std::sync::mpsc;
use std::thread;

let (tx, rx) = mpsc::channel();           // tx : l'émetteur · rx : le récepteur

thread::spawn(move || {
    let message = String::from("résultat");
    tx.send(message).unwrap();            // la possession de `message` ENTRE dans le canal
});                                       // (move : ce thread possède tx et message)

let recu = rx.recv().unwrap();            // …et RESSORT ici, côté récepteur
println!("{recu}");                       // un seul propriétaire à tout instant : rien n'est partagé

mpsc::channel() rend une paire : un émetteur (tx) et un récepteur (rx). send dépose une valeur dans le canal, recv la retire de l'autre côté. Ce qui circule n'est pas une référence partagée mais la valeur elle-même, dont la possession voyage. Compare au Arc<Mutex> du №2 : là, plusieurs threads pointaient vers une mémoire commune et se relayaient par un verrou ; ici, la donnée n'est jamais commune — elle passe de main en main, et une seule main la tient à la fois.mpsc::channel() returns a pair: a sender (tx) and a receiver (rx). send drops a value into the channel, recv pulls it out the other side. What flows isn't a shared reference but the value itself, whose ownership travels. Compare to №2's Arc<Mutex>: there, several threads pointed at common memory and took turns via a lock; here, the data is never common — it passes hand to hand, and only one hand holds it at a time.

Le réflexe du numéroThe issue's reflex

« Avant de dégainer Arc<Mutex>, ai-je vraiment besoin de partager — ou puis-je transférer la possession par un message ? » Quand des threads se passent du travail (une file de tâches, un pipeline, des résultats à collecter), le canal est presque toujours plus simple et plus sûr que la mémoire partagée : pas de verrou, pas de section critique, pas de deadlock sur des données. On ne synchronise pas l'accès — on supprime le besoin de synchroniser.'Before reaching for Arc<Mutex>, do I truly need to share — or can I transfer ownership through a message?' When threads hand off work (a task queue, a pipeline, results to collect), the channel is almost always simpler and safer than shared memory: no lock, no critical section, no deadlock on data. You don't synchronize access — you remove the need to synchronize.

mpsc : ce que disent les quatre lettresmpsc: what the four letters say

multi-producer, single-consumer. Plusieurs émetteurs peuvent alimenter le même canal (l'émetteur se clone) ; un seul récepteur en sort les messages. C'est le canal de la std, dans std::sync::mpsc. Le modèle couvre l'immense majorité des cas : N ouvriers qui rapportent à un collecteur. Pour plusieurs récepteurs (multi-consumer) ou des canaux plus rapides, on nommera crossbeam au chapitre 3 — mais le réflexe et la sûreté sont identiques.multi-producer, single-consumer. Several senders can feed the same channel (the sender clones); one receiver pulls the messages out. It's the std's channel, in std::sync::mpsc. The model covers the vast majority of cases: N workers reporting to one collector. For several receivers (multi-consumer) or faster channels, we'll name crossbeam in chapter 3 — but the reflex and the safety are identical.

№ 02La surpriseThe surprise4 min

Ce qu'on envoie, on ne l'a plus : send déplace, il ne copie pas.What you send, you no longer have: send moves, it doesn't copy.

Pour qui vient d'un langage à GC, c'est la surprise du numéro, et elle est libératrice. Dans ces langages, « envoyer » un objet dans une file ou à un thread passe une référence : l'émetteur garde la sienne, et rien ne l'empêche de continuer à muter l'objet qu'un autre thread est en train de lire. C'est le data race classique, silencieux, qui ne se révèle qu'à l'exécution sous charge. En Rust, send(valeur) déplace la valeur : sa possession entre dans le canal, et l'émetteur n'a plus le droit d'y toucher — le compilateur le lui refuse, avec le E0382 du Vol 1. Ce n'est pas une restriction arbitraire : c'est précisément ce qui garantit qu'à tout instant, un seul thread possède la donnée. La sûreté du canal n'est pas dans le canal — elle est dans le move.For someone coming from a GC language, this is the issue's surprise, and it's liberating. In those languages, 'sending' an object to a queue or a thread passes a reference: the sender keeps its own, and nothing stops it from continuing to mutate the object another thread is reading. That's the classic data race, silent, surfacing only at runtime under load. In Rust, send(value) moves the value: its ownership enters the channel, and the sender no longer has the right to touch it — the compiler refuses, with Vol 1's E0382. It's not an arbitrary restriction: it's exactly what guarantees that, at any instant, a single thread owns the data. The channel's safety isn't in the channel — it's in the move.

Après send(), l'émetteur n'a plus la donnéeAfter send(), the sender no longer has the data
let (tx, rx) = mpsc::channel();

let message = String::from("résultat");
tx.send(message).unwrap();           // `message` est DÉPLACÉ dans le canal

println!("{message}");               // ✕ error[E0382]: borrow of moved value: `message`
//   après send(), l'émetteur n'a plus rien : la donnée appartient au canal, puis
//   au récepteur. impossible de la lire des deux côtés → aucun aliasing → aucune course.
let _ = rx.recv();

L'erreur est, mot pour mot, celle du Vol 1 sur un String déplacé. Rien de spécifique aux threads : send prend la valeur (sa signature est send(self, t: T) — T par valeur), donc l'émetteur la perd. C'est ce qui rend le canal sûr par construction : puisque l'émetteur ne peut plus lire ce qu'il a envoyé, il ne peut pas y avoir deux threads sur la même donnée. Le compilateur transforme une discipline qu'on devait tenir à la main (« ne touche plus à ce que tu as envoyé ») en un théorème qu'il vérifie pour toi.The error is, word for word, Vol 1's on a moved String. Nothing thread-specific: send takes the value (its signature is send(self, t: T) — T by value), so the sender loses it. That's what makes the channel safe by construction: since the sender can no longer read what it sent, there can't be two threads on the same data. The compiler turns a discipline you had to hold by hand ('don't touch what you've sent') into a theorem it verifies for you.

La sûreté du canal n'est pas dans le canal — elle est dans le move : ce qu'on a envoyé, on ne peut plus le lire, donc personne ne le partage.The channel's safety isn't in the channel — it's in the move: what you've sent, you can no longer read, so no one shares it.
№ 03La mécaniqueThe mechanics5 min

Plusieurs émetteurs, un récepteur — et un canal qui se ferme quand le dernier tx meurt.Several senders, one receiver — and a channel that closes when the last tx dies.

mpsc se lit dans ses deux moitiés. Côté émission, le Sender est Clone : on en fait autant de copies qu'on a de producteurs, et tous alimentent la même file — c'est le « multi-producer ». Côté réception, le Receiver est unique, non clonable — le « single-consumer ». send() dépose une valeur (en la déplaçant) ; recv() en retire une, bloquant jusqu'à ce qu'elle arrive, et rend un Result. Reste la question décisive : comment le récepteur sait-il qu'il n'y aura plus rien ? La réponse est, une fois de plus, la possession. Quand le dernier Sender est détruit, le canal se ferme ; recv() rend alors une erreur, et la boucle for sur rx se termine d'elle-même. On ne compte pas les messages : on observe la mort des émetteurs.mpsc reads in its two halves. On the sending side, the Sender is Clone: you make as many copies as you have producers, and all feed the same queue — that's 'multi-producer'. On the receiving side, the Receiver is unique, non-clonable — 'single-consumer'. send() deposits a value (by moving it); recv() pulls one out, blocking until it arrives, and returns a Result. The decisive question remains: how does the receiver know nothing more will come? The answer is, once again, ownership. When the last Sender is destroyed, the channel closes; recv() then returns an error, and the for loop over rx ends on its own. You don't count messages: you observe the senders' death.

MPSC — N émetteurs clonés, une file, un récepteur uniqueMPSC — N cloned senders, one queue, one unique receiver
tx
tx.clone()
tx.clone()
Sender : Clone ✓Sender: Clone ✓
▸
file FIFOFIFO queue
≋≋≋
▸
rx
Receiver : uniqueReceiver: unique
Trois producteurs, une collecteThree producers, one collection
use std::sync::mpsc;
use std::thread;

let (tx, rx) = mpsc::channel();

for id in 0..3 {
    let tx = tx.clone();                  // Sender est Clone : plusieurs PRODUCTEURS
    thread::spawn(move || {
        tx.send(format!("de {id}")).unwrap();
    });                                   // chaque clone de tx est lâché en fin de thread
}
drop(tx);                                 // ← on lâche le tx ORIGINEL (sinon le canal ne ferme jamais)

for recu in rx {                          // itère tant qu'un Sender vit ; déballe automatiquement
    println!("{recu}");
}                                         // la boucle se termine quand le DERNIER Sender meurt

Chaque thread reçoit son propre clone de tx (déplacé dans sa closure, comme au №1). Le détail qui piège tout le monde une fois : le drop(tx) sur l'émetteur originel. Sans lui, il resterait un Sender vivant — celui de la fonction principale — et la boucle for recu in rx attendrait éternellement un message de plus. La boucle se termine exactement quand le dernier Sender disparaît : les trois clones meurent en fin de thread, l'original par le drop explicite. C'est le sujet entier du chapitre 4.Each thread receives its own clone of tx (moved into its closure, as in №1). The detail that traps everyone once: the drop(tx) on the original sender. Without it, one live Sender would remain — the main function's — and the for recu in rx loop would wait forever for one more message. The loop ends exactly when the last Sender vanishes: the three clones die at their thread's end, the original by the explicit drop. That's the whole subject of chapter 4.

recv, try_recv, et le Result qui dit « c'est fini »recv, try_recv, and the Result that says 'it's over'
let trois = rx.recv();           // BLOQUE jusqu'au prochain message (ou Err si canal fermé)
let peut_etre = rx.try_recv();   // NE bloque pas : Ok(v), ou Err(Empty), ou Err(Disconnected)

// recv() rend Result<T, RecvError> : l'Err signale « plus aucun Sender, rien ne viendra ».
// c'est ainsi qu'un récepteur sait s'arrêter — pas par un compteur, mais par la fermeture
// du canal, déclenchée par le drop du dernier émetteur. la possession, encore, fait le travail.
Borné, et au-delà de la std.Bounded, and beyond the std.

channel() est non borné : send() ne bloque jamais, mais une file qui grossit plus vite qu'on ne la vide gonfle la mémoire sans limite. sync_channel(n) impose une capacité : une fois pleine, send() bloque jusqu'à ce qu'on consomme — c'est la contre-pression (backpressure), qui ralentit les producteurs trop rapides. Pour plusieurs récepteurs (mpmc), un select! sur plusieurs canaux, ou de meilleures performances, la caisse crossbeam-channel est le choix de référence hors std. Le modèle mental — la possession qui traverse — ne change pas d'un iota.channel() is unbounded: send() never blocks, but a queue growing faster than it's drained inflates memory without limit. sync_channel(n) imposes a capacity: once full, send() blocks until something is consumed — that's backpressure, slowing down too-fast producers. For several receivers (mpmc), a select! over multiple channels, or better performance, the crossbeam-channel crate is the go-to outside std. The mental model — ownership crossing — doesn't change one bit.

Un récepteur ne compte pas les messages — il observe la mort des émetteurs : le canal se ferme quand le dernier Sender est lâché.A receiver doesn't count messages — it observes the senders' death: the channel closes when the last Sender is dropped.
№ 04Le piègeThe trap4 min

Un Sender oublié, vivant — et le récepteur attend un message pour toujours.One forgotten Sender, still alive — and the receiver waits for a message forever.

Le canal supprime le deadlock sur les données — mais il en introduit un qui lui est propre, et qui piège tout le monde une fois. La boucle for sur un récepteur ne se termine que lorsque le canal se ferme, et le canal ne se ferme que lorsque le dernier Sender est détruit. Or il est facile d'en laisser un vivant sans le vouloir : l'émetteur originel qu'on a cloné pour les threads mais jamais lâché, un tx capturé dans une structure, un clone gardé « au cas où ». Tant qu'un seul Sender respire, le récepteur croit que d'autres messages peuvent arriver, et recv() bloque indéfiniment. Le programme se fige — pas sur un verrou, mais sur l'attente d'un message qui ne viendra jamais. Ça compile sans le moindre avertissement : comme le deadlock du №2, c'est une faute de logique, pas de type. La discipline tient en trois mots : compte tes Senders.The channel removes the deadlock on data — but it introduces one of its own, which traps everyone once. The for loop over a receiver only ends when the channel closes, and the channel only closes when the last Sender is destroyed. Yet it's easy to leave one alive unwittingly: the original sender you cloned for the threads but never dropped, a tx captured in a struct, a clone kept 'just in case'. As long as a single Sender breathes, the receiver believes more messages may come, and recv() blocks indefinitely. The program freezes — not on a lock, but on waiting for a message that will never come. It compiles without the slightest warning: like №2's deadlock, it's a logic fault, not a type fault. The discipline fits in three words: count your Senders.

Le contre-exemple — l'émetteur originel jamais lâchéThe counter-example — the original sender never dropped
let (tx, rx) = mpsc::channel();

for id in 0..3 {
    let tx = tx.clone();
    thread::spawn(move || { tx.send(id).unwrap(); });
}
// ✕ on a OUBLIÉ de lâcher `tx` : l'émetteur originel reste vivant dans ce scope.

for recu in rx {                     // les 3 messages arrivent… puis la boucle ATTEND
    println!("{recu}");              // un 4ᵉ message qui ne viendra jamais.
}                                    // ↻ le programme se FIGE — pas un deadlock de verrou,
                                     //   mais un canal qui ne ferme pas. et ça compile.
3 × tx.clone()
tx (oublié, vivant)(forgotten, alive)
1 Sender respire encore1 Sender still breathes
▸
canalchannel
JAMAIS ferméNEVER closed
▸
for recu in rx
↻
attend pour toujourswaits forever
Le bon réflexe — lâcher l'émetteur originel pour fermer le canalThe right reflex — drop the original sender to close the channel
let (tx, rx) = mpsc::channel();

for id in 0..3 {
    let tx = tx.clone();
    thread::spawn(move || { tx.send(id).unwrap(); });
}
drop(tx);                            // ✓ on lâche explicitement l'émetteur originel

for recu in rx {                     // dès que les 3 clones ET l'original sont lâchés,
    println!("{recu}");              // le canal ferme, la boucle se termine proprement.
}
// règle : la boucle for sur rx finit EXACTEMENT quand le dernier Sender meurt. compte tes tx.

Le drop(tx) n'est pas une cérémonie : c'est l'acte qui annonce la fin. Souvent on l'évite plus élégamment en ne gardant jamais l'émetteur originel — on le move entièrement dans le code producteur, ou on enferme les tx dans un scope qui se referme. Mais quel que soit le style, la règle est invariable : la boucle for sur rx se termine quand, et seulement quand, le dernier Sender est détruit. Si ton programme se fige à la fin, ne cherche pas un verrou : cherche le tx que tu as oublié de laisser mourir.The drop(tx) isn't ceremony: it's the act that announces the end. Often you avoid it more elegantly by never keeping the original sender — you move it entirely into the producer code, or enclose the txs in a scope that closes. But whatever the style, the rule is invariable: the for loop over rx ends when, and only when, the last Sender is destroyed. If your program freezes at the end, don't look for a lock: look for the tx you forgot to let die.

L'autre frontière — un canal ne diffuse pasThe other boundary — a channel doesn't broadcast

Le réflexe « message » a sa limite, symétrique du « Mutex partout » du №2. Un canal transfère la possession à un consommateur : ce qui entre en sort une seule fois, chez un seul receveur. Donc si plusieurs threads ont besoin de lire la même donnée immuable — une config, une table de référence —, le canal est le mauvais outil : il faudrait la cloner pour chacun. C'est exactement le cas où Arc<T> (sans Mutex, lecture seule) reste imbattable. Transférer ≠ partager : à chaque besoin son outil.The 'message' reflex has its limit, symmetric to №2's 'Mutex everywhere'. A channel transfers ownership to one consumer: what goes in comes out once, at a single receiver. So if several threads need to read the same immutable data — a config, a lookup table — the channel is the wrong tool: you'd have to clone it for each. That's exactly where Arc<T> (no Mutex, read-only) stays unbeatable. Transfer ≠ share: to each need its tool.

Le test mentalThe mental test

« La donnée a-t-elle un seul destinataire à la fois, ou plusieurs lecteurs simultanés ? » Un seul destinataire qui en prend possession → canal : la donnée voyage, personne ne la partage. Plusieurs lecteurs de la même donnée immuable → Arc : tout le monde lit, personne ne mute. Et plusieurs threads qui mutent une donnée commune → Arc<Mutex> du №2, en dernier recours. Trois besoins, trois outils ; le piège est d'en forcer un dans le rôle d'un autre.'Does the data have a single recipient at a time, or several simultaneous readers?' One recipient taking ownership → channel: the data travels, no one shares it. Several readers of the same immutable data → Arc: everyone reads, no one mutates. And several threads mutating common data → №2's Arc<Mutex>, as a last resort. Three needs, three tools; the trap is forcing one into another's role.

Si le programme se fige à la fin, ne cherche pas un verrou : cherche le Sender que tu n'as pas laissé mourir.If the program freezes at the end, don't look for a lock: look for the Sender you didn't let die.
№ 05Bilan & éditoWrap-up & editorial3 min

La concurrence sans peur, c'était une seule règle — aliasing XOR mutation — à travers les threads.Fearless concurrency was one rule — aliasing XOR mutation — across threads.

Ce numéro clôt le Vol 7 sur le geste le plus radical : ne plus partager du tout. Un canal transfère la possession d'un message — le move du Vol 1, à travers une frontière de threads — si bien qu'à tout instant un seul thread détient la donnée. Plus rien de commun, donc plus de course : la sûreté n'est pas dans le canal, elle est dans le move. mpsc relie N producteurs (le Sender se clone) à un consommateur unique ; le canal se ferme quand le dernier émetteur meurt, et c'est ainsi que le récepteur sait s'arrêter. Le piège est le miroir du précédent : un Sender oublié laisse le canal ouvert, et la boucle se fige sur un message qui ne viendra jamais. Trois besoins, trois outils — transférer (canal), lire à plusieurs (Arc), muter à plusieurs (Arc<Mutex>) — et le réflexe est de ne jamais forcer l'un dans le rôle d'un autre.This issue closes Vol 7 on the most radical move: stop sharing entirely. A channel transfers ownership of a message — Vol 1's move, across a thread boundary — so that at any instant a single thread holds the data. Nothing common, hence no race: the safety isn't in the channel, it's in the move. mpsc links N producers (the Sender clones) to a single consumer; the channel closes when the last sender dies, and that's how the receiver knows to stop. The trap mirrors the previous one: a forgotten Sender leaves the channel open, and the loop freezes on a message that will never come. Three needs, three tools — transfer (channel), read together (Arc), mutate together (Arc<Mutex>) — and the reflex is to never force one into another's role.

Le besoin entre threadsThe cross-thread needL'outil — et ce qu'il fait à la possessionThe tool — and what it does to ownership
Donner à un threadGive to a threadmove : la possession part, l'original devient inaccessible (№1).move: ownership leaves, the original becomes inaccessible (№1).
Prêter le temps d'un calculLend for a computationthread::scope : emprunt par &, car les threads joignent avant la fin (№1).thread::scope: borrow by &, since threads join before the end (№1).
Lire à plusieursRead togetherArc<T> : possession partagée, en lecture seule (№2).Arc<T>: shared ownership, read-only (№2).
Muter à plusieursMutate togetherArc<Mutex<T>> : exclusivité rétablie par un verrou — gare au deadlock (№2).Arc<Mutex<T>>: exclusivity restored by a lock — beware the deadlock (№2).
Se passer le travailHand off workmpsc : la possession traverse le canal — gare au Sender oublié (№3).mpsc: ownership crosses the channel — beware the forgotten Sender (№3).
Rien dans ce volume n'était nouveau : la règle du Vol 1, projetée sur les threads. Le data race ne s'attrape pas par un test — il ne compile pas.Nothing in this volume was new: Vol 1's rule, projected onto threads. A data race isn't caught by a test — it doesn't compile.
Mot de l'éditeurFrom the editor

Sept volumes pour lancer du travail en parallèle. Le huitième : attendre sans bloquer.Seven volumes to launch work in parallel. The eighth: wait without blocking.

Le Vol 7 a tenu une seule promesse, déclinée quatre fois. Donner une donnée à un thread (move), la lui prêter (scope), la partager pour muter (Arc<Mutex>), la transmettre par message (canal) : à chaque fois, c'était « aliasing XOR mutation » du Vol 1, appliquée par-delà la frontière des threads. La concurrence sans peur n'a jamais été un mécanisme de plus — c'était la possession, qui ne s'arrête pas au bord d'un thread. Mais tout ce volume parlait de parallélisme : faire tourner plusieurs choses en même temps, sur plusieurs cœurs. Il existe un autre besoin, qu'on confond sans cesse avec celui-là.Vol 7 kept a single promise, declined four times. Give data to a thread (move), lend it (scope), share it to mutate (Arc<Mutex>), pass it by message (channel): each time, it was Vol 1's 'aliasing XOR mutation', applied beyond the thread boundary. Fearless concurrency was never one more mechanism — it was ownership, not stopping at a thread's edge. But this whole volume spoke of parallelism: running several things at once, on several cores. There's another need, constantly confused with that one.

Vol 8 : async, la concurrence sans threads. La distinction qui ouvre le volume : ai-je besoin de parallélisme (calculer en même temps), ou seulement d'attendre sans bloquer (mille connexions réseau dont chacune dort la plupart du temps) ? Pour le second, lancer mille threads est un gâchis ; on veut une seule pile qui jongle entre mille tâches en sommeil. C'est async/await — et le piège, pour qui vient de JS, c'est que la syntaxe est identique mais le modèle ne l'est pas. On commencera par le plus contre-intuitif : en Rust, un Future ne fait rien tant qu'on ne le poll pas. Il décrit un calcul ; il ne le lance pas. La promesse qu'on croyait partie n'a même pas démarré.Vol 8: async, concurrency without threads. The distinction that opens the volume: do I need parallelism (computing at the same time), or only to wait without blocking (a thousand network connections, each asleep most of the time)? For the latter, spawning a thousand threads is waste; you want a single stack juggling a thousand sleeping tasks. That's async/await — and the trap, for someone from JS, is that the syntax is identical but the model isn't. We'll start with the most counter-intuitive: in Rust, a Future does nothing until you poll it. It describes a computation; it doesn't launch it. The promise you thought had left hasn't even started.