Channels
Concurrency · Lesson 43
Channels send values from one thread to another. Sending a value moves it into the channel.
Coming from Gompsc::channel() is a queue that grows, so send does not wait for the receiver the way a send on make(chan T) does. sync_channel(n) is make(chan T, n), and sync_channel(0) is the unbuffered kind. There is no close: the channel ends when every sender is dropped, and there is no select in the standard library.
Coming from KotlinA channel is like a LinkedBlockingQueue, but send moves the value in, so the sender can't touch it afterwards, and only one thread receives (a Kotlin coroutine Channel may have several receivers; a Rust Receiver cannot be shared). When every sender is dropped the channel closes and for msg in rx ends, so no "DONE" marker is needed.
Coming from PythonA channel is like queue.Queue, and for msg in rx stops by itself when every sender is gone, so you don't need a None marker (or shutdown(), new in Python 3.13). send moves the value, so the sender can't change it afterwards.
Coming from TypeScriptA channel is like postMessage between workers, except that postMessage copies the data (unless you transfer it) and send moves it. for msg in rx blocks a thread, with no callback, until every sender has been dropped; a MessagePort has no such count.
Coming from Javampsc::channel() is a LinkedBlockingQueue, but send moves the value, so the sender can't touch it afterwards. The channel closes by itself when every sender is dropped, so no DONE marker is needed.
Coming from C#mpsc::channel() is Channel.CreateUnbounded<T>(), and for msg in rx is await foreach over ReadAllAsync(), except it blocks a thread instead of awaiting. Dropping the last tx closes it like Writer.Complete(), and send moves the value so the sender can't change it later.
Coming from CC has no channels, so you'd build a queue with a mutex and a condition variable (or use a pipe for bytes). mpsc::channel() is that queue, ready made; it closes when the last sender is dropped, and the compiler stops the sender from using a value after sending it.
Coming from C++The standard library has no channel, so mpsc::channel() replaces your mutex, condition_variable and queue class. send moves like std::move, but Rust then rejects any later use of the moved-from variable, which C++ accepts.
The quick comparison
use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel(); // tx sends, rx receives
thread::spawn(move || { for i in 1..=3 { let msg = format!("job {i}"); tx.send(msg).unwrap(); // msg moves into the channel } }); // tx goes away here: channel closes
// ends when the channel closes for msg in rx { println!("got {msg}"); }}got job 1 got job 2 got job 3
package main
import "fmt"
func main() { ch := make(chan string)
go func() { for i := 1; i <= 3; i++ { ch <- fmt.Sprintf("job %d", i) } close(ch) }()
for msg := range ch { fmt.Println("got", msg) }}got job 1 got job 2 got job 3
import java.util.concurrent.LinkedBlockingQueueimport kotlin.concurrent.thread
fun main() { val queue = LinkedBlockingQueue<String>()
val worker = thread { for (i in 1..3) { queue.put("job $i") } queue.put("DONE") // no built-in close, so send a marker }
while (true) { val msg = queue.take() if (msg == "DONE") break println("got $msg") } worker.join()}got job 1 got job 2 got job 3
import queueimport threading
q: queue.Queue[str | None] = queue.Queue()
def produce() -> None: for i in range(1, 4): q.put(f"job {i}") q.put(None) # no "close" on a Queue, so send a marker
threading.Thread(target=produce).start()
while (msg := q.get()) is not None: print(f"got {msg}")got job 1 got job 2 got job 3
import { Worker, isMainThread, parentPort } from "node:worker_threads";
if (isMainThread) { const worker = new Worker(__filename); // messages arrive in the order they were sent worker.on("message", (msg: string) => console.log(`got ${msg}`));} else { for (let i = 1; i <= 3; i++) { parentPort!.postMessage(`job ${i}`); // the message is copied across }} // the worker ends here and the program exits once its messages are handledgot job 1 got job 2 got job 3
import java.util.concurrent.LinkedBlockingQueue;
public class Main { public static void main(String[] args) throws InterruptedException { var queue = new LinkedBlockingQueue<String>();
var worker = Thread.startVirtualThread(() -> { for (int i = 1; i <= 3; i++) { queue.add("job " + i); } queue.add("DONE"); // queues can't be closed, so send a marker });
while (true) { var msg = queue.take(); if (msg.equals("DONE")) break; System.out.println("got " + msg); } worker.join(); }}got job 1 got job 2 got job 3
using System.Threading.Channels;
var ch = Channel.CreateUnbounded<string>();
_ = Task.Run(async () =>{ for (var i = 1; i <= 3; i++) { await ch.Writer.WriteAsync($"job {i}"); } ch.Writer.Complete(); // close the channel});
await foreach (var msg in ch.Reader.ReadAllAsync()) // ends when it closes{ Console.WriteLine($"got {msg}");}got job 1 got job 2 got job 3
#include <assert.h>#include <pthread.h>#include <stdbool.h>#include <stdio.h>#include <stdlib.h>
// C has no channels: a small queue guarded by a mutex and a condition variablestruct Channel { pthread_mutex_t lock; pthread_cond_t ready; char *items[8]; // a sketch: room for 8 messages in all, no wrap-around int head, tail; bool closed;};
void send(struct Channel *ch, char *msg) { pthread_mutex_lock(&ch->lock); assert(ch->tail < 8); // a real queue would wait here until there is room ch->items[ch->tail++] = msg; // the receiver now owns msg pthread_cond_signal(&ch->ready); pthread_mutex_unlock(&ch->lock);}
void close_channel(struct Channel *ch) { pthread_mutex_lock(&ch->lock); ch->closed = true; pthread_cond_broadcast(&ch->ready); // wake every receiver pthread_mutex_unlock(&ch->lock);}
// returns NULL once the channel is closed and emptychar *recv(struct Channel *ch) { pthread_mutex_lock(&ch->lock); while (ch->head == ch->tail && !ch->closed) { pthread_cond_wait(&ch->ready, &ch->lock); } char *msg = ch->head < ch->tail ? ch->items[ch->head++] : NULL; pthread_mutex_unlock(&ch->lock); return msg;}
void *producer(void *arg) { struct Channel *ch = arg; for (int i = 1; i <= 3; i++) { char *msg = malloc(16); snprintf(msg, 16, "job %d", i); send(ch, msg); } close_channel(ch); return NULL;}
int main(void) { struct Channel ch = { .lock = PTHREAD_MUTEX_INITIALIZER, .ready = PTHREAD_COND_INITIALIZER, }; pthread_t t; pthread_create(&t, NULL, producer, &ch);
char *msg; while ((msg = recv(&ch)) != NULL) { // ends when the channel closes printf("got %s\n", msg); free(msg); } pthread_join(t, NULL); return 0;}got job 1 got job 2 got job 3
#include <condition_variable>#include <format>#include <iostream>#include <mutex>#include <optional>#include <queue>#include <string>#include <thread>#include <utility>
// The standard library has no channel, so here is a small one.template <class T>class Channel {public: void send(T value) { { std::lock_guard lock(m_); q_.push(std::move(value)); } cv_.notify_one(); } void close() { { std::lock_guard lock(m_); closed_ = true; } cv_.notify_all(); } std::optional<T> recv() { // empty optional once closed and drained std::unique_lock lock(m_); cv_.wait(lock, [&] { return !q_.empty() || closed_; }); if (q_.empty()) return std::nullopt; T value = std::move(q_.front()); q_.pop(); return value; }
private: std::mutex m_; std::condition_variable cv_; std::queue<T> q_; bool closed_ = false;};
int main() { Channel<std::string> ch;
std::jthread producer([&ch] { for (int i = 1; i <= 3; ++i) { ch.send(std::format("job {}", i)); // moved into the channel } ch.close(); // no more messages });
while (auto msg = ch.recv()) { // ends when the channel closes std::cout << "got " << *msg << "\n"; }}got job 1 got job 2 got job 3
What's different
mpscmeans multiple producers, single consumer. Clonetxto have several senders; the receiver cannot be cloned.- You never call
close. The channel closes when every sender has gone away, here when the thread ends, andfor msg in rxends with it. sendreturns aResult, because the receiving side might already be gone..unwrap()here means "crash if nobody is listening".
Changing a value after sending it
Looks fine: in TypeScript it runs fine; in C++ it runs but gives a wrong answer; in Go, Kotlin, Python, Java, C# and C it runs but has a data race. Rust won't compile it.
Broken in Go. Rust catches it.Broken in Kotlin. Rust catches it.Broken in Python. Rust catches it.Works in TypeScript. In Rust it's just a rule.Broken in Java. Rust catches it.Broken in C#. Rust catches it.Broken in C. Rust catches it.Broken in C++. Rust catches it.use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let mut msg = vec![1, 2]; tx.send(msg).unwrap(); msg.push(3); }); println!("{:?}", rx.recv().unwrap());}package main
import ( "fmt" "time")
func main() { ch := make(chan []int) done := make(chan bool) go func() { got := <-ch time.Sleep(10 * time.Millisecond) fmt.Println("receiver sees", got) done <- true }() msg := []int{1, 2} ch <- msg msg[0] = 99 // sender keeps changing what it already sent <-done}==================
WARNING: DATA RACE
Read at 0x············ by goroutine 8:
runtime.typedmemmove()
/opt/go/src/runtime/mbarrier.go:152 +0x7c
reflect.packEfaceData()
/opt/go/src/reflect/value.go:143 +0xe8
reflect.packEface()
/opt/go/src/reflect/value.go:126 +0x15c
reflect.valueInterface()
/opt/go/src/reflect/value.go:1512 +0x1b0
reflect.Value.Interface()
/opt/go/src/reflect/value.go:1490 +0x78
fmt.(*pp).printValue()
/opt/go/src/fmt/print.go:759 +0x64
fmt.(*pp).printValue()
/opt/go/src/fmt/print.go:898 +0x1a68
fmt.(*pp).printArg()
/opt/go/src/fmt/print.go:749 +0x8ec
fmt.(*pp).doPrintln()
/opt/go/src/fmt/print.go:1205 +0x40
fmt.Fprintln()
/opt/go/src/fmt/print.go:297 +0x48
fmt.Println()
/opt/go/src/fmt/print.go:307 +0xe0
main.main.func1()
main.go:14 +0x64
Previous write at 0x············ by main goroutine:
main.main()
main.go:19 +0x12c
Goroutine 8 (running) created at:
main.main()
main.go:11 +0xdc
==================
receiver sees [99 2]
Found 1 data race(s)import java.util.concurrent.LinkedBlockingQueueimport kotlin.concurrent.thread
fun main() { val queue = LinkedBlockingQueue<MutableList<Int>>() val receiver = thread { val got = queue.take() Thread.sleep(10) println("receiver sees $got") } val msg = mutableListOf(1, 2) queue.put(msg) msg[0] = 99 // sender keeps changing what it already sent receiver.join()}receiver sees [99, 2]
import queueimport threadingimport time
q = queue.Queue()
def receive(): got = q.get() time.sleep(0.01) print("receiver sees", got)
r = threading.Thread(target=receive)r.start()msg = [1, 2]q.put(msg)msg[0] = 99 # sender keeps changing what it already sentr.join()receiver sees [99, 2]
import { Worker, isMainThread, parentPort } from "node:worker_threads";
if (isMainThread) { const worker = new Worker(__filename); worker.on("message", (msg: number[]) => console.log(msg));} else { const msg = [1, 2]; parentPort!.postMessage(msg); msg.push(3); // change it after sending}[ 1, 2 ]
import java.util.ArrayList;import java.util.List;import java.util.concurrent.LinkedBlockingQueue;
public class Main { public static void main(String[] args) throws InterruptedException { var queue = new LinkedBlockingQueue<List<Integer>>(); Thread.startVirtualThread(() -> { var msg = new ArrayList<>(List.of(1, 2)); queue.add(msg); msg.add(3); // sender keeps changing what it already sent }); var got = queue.take(); Thread.sleep(100); System.out.println(got); }}[1, 2, 3]
using System.Threading.Channels;
var ch = Channel.CreateUnbounded<List<int>>();
var receiver = Task.Run(async () =>{ var got = await ch.Reader.ReadAsync(); await Task.Delay(50); // a little work before looking at it Console.WriteLine($"receiver sees [{string.Join(", ", got)}]");});
List<int> msg = [1, 2];await ch.Writer.WriteAsync(msg);msg[0] = 99; // sender keeps changing what it already sentawait receiver;receiver sees [99, 2]
#define _POSIX_C_SOURCE 200809L // for nanosleep#include <pthread.h>#include <stdio.h>#include <time.h>
// a one-slot "channel": a pointer handed over under a lockpthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;pthread_cond_t ready = PTHREAD_COND_INITIALIZER;int *slot = NULL;
void sleep_ms(long ms) { struct timespec ts = {0, ms * 1000000L}; nanosleep(&ts, NULL);}
void *receiver(void *arg) { (void)arg; pthread_mutex_lock(&lock); while (slot == NULL) pthread_cond_wait(&ready, &lock); int *got = slot; pthread_mutex_unlock(&lock);
sleep_ms(10); printf("receiver sees [%d, %d]\n", got[0], got[1]); return NULL;}
int main(void) { pthread_t t; pthread_create(&t, NULL, receiver, NULL);
int msg[2] = {1, 2}; pthread_mutex_lock(&lock); slot = msg; // send pthread_cond_signal(&ready); pthread_mutex_unlock(&lock);
msg[0] = 99; // sender keeps changing what it already sent pthread_join(t, NULL); return 0;}==================
WARNING: ThreadSanitizer: data race (pid=1)
Read of size 4 at 0x············ by thread T1:
#0 receiver main.c:24 (main+0xd74)
Previous write of size 4 at 0x············ by main thread:
#0 main main.c:38 (main+0xe64)
As if synchronized via sleep:
#0 nanosleep ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:397 (libtsan.so.2+0x59f60)
#1 sleep_ms main.c:13 (main+0xcc0)
#2 receiver main.c:23 (main+0xd6c)
Location is stack of main thread.
Location is global '<null>' at 0x············ ([stack]+0x209b0)
Thread T1 (tid=1, running) created by main thread at:
#0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
#1 main main.c:30 (main+0xdf8)
SUMMARY: ThreadSanitizer: data race main.c:24 in receiver
==================
receiver sees [99, 2]
ThreadSanitizer: reported 1 warnings#include <condition_variable>#include <iostream>#include <mutex>#include <queue>#include <thread>#include <utility>#include <vector>
template <class T>class Channel { // the same small channel as the lesson, cut downpublic: void send(T v) { { std::lock_guard lock(m_); q_.push(std::move(v)); } cv_.notify_one(); } T recv() { std::unique_lock lock(m_); cv_.wait(lock, [&] { return !q_.empty(); }); T v = std::move(q_.front()); q_.pop(); return v; }private: std::mutex m_; std::condition_variable cv_; std::queue<T> q_;};
void print(const char* label, const std::vector<int>& xs) { std::cout << label << " ["; for (std::size_t i = 0; i < xs.size(); ++i) std::cout << (i ? ", " : "") << xs[i]; std::cout << "]\n";}
int main() { Channel<std::vector<int>> ch; std::vector<int> msg{1, 2}; std::thread sender([&] { ch.send(std::move(msg)); msg.push_back(3); }); print("received", ch.recv()); sender.join(); print("sender's msg is now", msg);}received [1, 2] sender's msg is now [3]
What does the compiler say? Guess first, then open.
error[E0382]: borrow of moved value: `msg` --> src/main.rs:9:9 | 7 | let mut msg = vec![1, 2]; | ------- move occurs because `msg` has type `Vec<i32>`, which does not implement the `Copy` trait 8 | tx.send(msg).unwrap(); | --- value moved here 9 | msg.push(3); | ^^^ value borrowed here after move
Go printed receiver sees [99 2]. The channel passed a slice that shares its array, so the sender's later change showed up on the other side. Built with the race detector, as here, it also reports a data race.
Kotlin printed receiver sees [99, 2]. The queue passed the same list object, not a copy, so the sender's change after put showed up on the other side.
Python prints receiver sees [99, 2]. The queue passes the same list object, not a copy, so the sender's change after put shows up on the other side.
Prints [ 1, 2 ]. postMessage copies the array when you send it, so the later push only changes the sender's copy.
The queue passes a reference, not a copy, so the receiver sees the 3 the sender added after sending. Nothing stops both threads from touching the same list.
The channel passes a reference to the same List, so the sender's later change shows up and the receiver sees [99, 2]. Send a copy, or stop touching it after sending.
C hands over a pointer, so the sender still has the same array. The receiver sees 99, and ThreadSanitizer reports the data race.
This compiles cleanly. std::move hands the vector to the channel and leaves msg in a valid but unspecified state (empty with libstdc++), so push_back(3) lands in a leftover vector the receiver never sees.
send(msg) moves msg into the channel. After that line, the sending thread doesn't own it anymore, so it can't touch it.
Once you send something, it belongs to the receiver. The sender can't touch it anymore, so two threads never fight over it, and the compiler checks this for you.
The fix
Compiles and runs.
use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let mut msg = vec![1, 2]; msg.push(3); // finish building it first tx.send(msg).unwrap(); // then hand it over }); println!("{:?}", rx.recv().unwrap());}[1, 2, 3]
The toolkit
Outputs on this page come from Linux on 64-bit ARM (the VM had 8 cores) with Rust 1.99.1, built in debug mode. Every program here prints the same thing on every run: it joins its threads, then sorts or counts before it prints. The Rust tables are asserted by programs the site runs. Threads here are OS threads, not goroutines or virtual threads, and recv blocks one. Threads introduced spawn and join; Shared state covers Arc and Mutex.
A channel is a queue with two ends
mpsc::channel() returns a pair: a Sender (tx) that puts values in, and a Receiver (rx) that takes them out, first in, first out. mpsc stands for “multiple producer, single consumer”: you can clone the sender, never the receiver, so one thread receives. Go channels, Java queues and C# channels accept several consumers; mpsc does not, and the pool shows what that costs.
use std::sync::mpsc;use std::thread;
fn main() { // tx is the sending end, rx the receiving end. T is inferred from the first send. let (tx, rx) = mpsc::channel();
let worker = thread::spawn(move || { for i in 1..=3 { tx.send(format!("job {i}")).unwrap(); } // tx is dropped here, when the closure ends });
for _ in 0..3 { println!("recv: {:?}", rx.recv()); } worker.join().unwrap();
// Nothing is queued and the only sender is gone, so waiting would be pointless. println!("recv after the worker ended: {:?}", rx.recv());}recv: Ok("job 1")
recv: Ok("job 2")
recv: Ok("job 3")
recv after the worker ended: Err(RecvError)send and recv both return a Result. recv waits for a value, and returns Err only when the queue is empty and every sender is gone.
Sending moves the value
send(value) takes value by value, so the sender no longer owns it (the gotcha is that error, with a Vec). A Copy integer is copied, so the variable stays usable. A Vec moves, and what moves is its three words; the numbers stay where they were:
use std::mem::size_of;use std::sync::mpsc;use std::thread;
fn main() { // A Copy value: send copies it, so n is still usable. let (tx, rx) = mpsc::channel(); let n = 7; tx.send(n).unwrap(); println!("n after send: {n}, received: {}", rx.recv().unwrap());
// A Vec: send moves it. The Vec is 3 words; its numbers stay where they were. let (tx, rx) = mpsc::channel::<Vec<i32>>(); let v = vec![10, 20, 30]; let before = v.as_ptr(); println!("size_of::<Vec<i32>>(): {} bytes", size_of::<Vec<i32>>());
tx.send(v).unwrap(); // v is moved: using it here would not compile let got = thread::spawn(move || rx.recv().unwrap()).join().unwrap();
println!("received: {got:?}"); println!( "same heap buffer after the trip: {}", got.as_ptr() == before );}n after send: 7, received: 7 size_of::<Vec<i32>>(): 24 bytes received: [10, 20, 30] same heap buffer after the trip: true
While a value is in flight one thread owns it, so it needs no lock. It must be Send, “allowed to move to another thread” (Send and Sync). An Rc is not, and the error comes from thread::spawn, because the closure holds the Receiver:
use std::rc::Rc;use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel(); tx.send(Rc::new(5)).unwrap();
let reader = thread::spawn(move || { println!("got {}", rx.recv().unwrap()); }); reader.join().unwrap();}error[E0277]: `Rc<i32>` cannot be sent between threads safely --> src/main.rs:9:32 | 9 | let reader = thread::spawn(move || { | __________________-------------_^ | | | | | required by a bound introduced by this call 10 | | println!("got {}", rx.recv().unwrap()); 11 | | }); | |_____^ `Rc<i32>` cannot be sent between threads safely | = help: the trait `Send` is not implemented for `Rc<i32>` = note: required for `std::sync::mpsc::Receiver<Rc<i32>>` to implement `Send` note: required because it's used within this closure --> src/main.rs:9:32 | 9 | let reader = thread::spawn(move || { | ^^^^^^^ ... (notes pointing into the standard library trimmed)
Read the notes in order: Rc<i32> is not Send, so Receiver<Rc<i32>> is not, so the closure that owns it is not, and thread::spawn demands a Send closure (Reading signatures).
Several producers
tx.clone() makes another sender for the same channel. Give each thread its own (Go calls this fan-in):
use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel();
let mut workers = Vec::new(); for id in 1..=3 { let tx = tx.clone(); // one more sender, same channel workers.push(thread::spawn(move || { for step in 1..=3 { tx.send((id, step)).unwrap(); } })); } drop(tx); // the original sender; without this the loop below would never end
let mut all = Vec::new(); let mut last_step = [0; 4]; for (id, step) in rx { // messages from ONE sender arrive in the order it sent them assert_eq!(step, last_step[id] + 1); last_step[id] = step; all.push((id, step)); } for w in workers { w.join().unwrap(); }
// the interleaving of different senders changes from run to run: sort before printing all.sort(); println!("received {} messages", all.len()); println!("sorted: {all:?}");}received 9 messages sorted: [(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)]
One sender’s messages arrive in the order it sent them (the assertion checks it). Different senders interleave differently on every run, so the program sorts before it prints. If order matters, put it in the message, as the first exercise does with an index.
When does for msg in rx end?
When the queue is empty and every sender has been dropped. The classic mistake is a main thread that keeps its own tx while it loops on rx: the workers finish, one sender is still alive, and the loop waits for a message that cannot come. No error is reported; the loop prints what arrived and then stops making progress. The mpsc docs say the program “will not complete” without the drop. A hang cannot be shown here, so this is the state just before it, measured with calls that give up:
use std::sync::mpsc::{self, TryRecvError};use std::thread;use std::time::Duration;
fn main() { let (tx, rx) = mpsc::channel();
// Two workers, each with its own clone. We join them, so both are finished. let workers: Vec<_> = (1..=2) .map(|id| { let tx = tx.clone(); thread::spawn(move || tx.send(format!("from worker {id}")).unwrap()) }) .collect(); for w in workers { w.join().unwrap(); }
// The workers are gone, but main still holds `tx`. let mut msgs = vec![rx.try_recv().unwrap(), rx.try_recv().unwrap()]; msgs.sort(); println!("queued messages: {msgs:?}"); println!("try_recv, tx alive: {:?}", rx.try_recv()); println!( "recv_timeout, tx alive: {:?}", rx.recv_timeout(Duration::from_millis(50)) ); // `for msg in rx` would wait here for ever: a sender exists that nobody will use.
drop(tx); println!("try_recv, tx dropped: {:?}", rx.try_recv()); println!("recv, tx dropped: {:?}", rx.recv()); assert_eq!(rx.try_recv(), Err(TryRecvError::Disconnected));}queued messages: ["from worker 1", "from worker 2"] try_recv, tx alive: Err(Empty) recv_timeout, tx alive: Err(Timeout) try_recv, tx dropped: Err(Disconnected) recv, tx dropped: Err(RecvError)
Empty means “nothing now, but a sender exists”, Disconnected “nothing, ever”, and Timeout is recv_timeout giving up. All the ways to receive:
| Call | Queue has a message | Empty, a sender exists | Empty, every sender dropped |
|---|---|---|---|
recv() |
the message | waits | Err(RecvError) |
try_recv() |
the message | Err(Empty) |
Err(Disconnected) |
recv_timeout(d) |
the message | waits up to d, then Err(Timeout) |
Err(Disconnected) |
for m in rx, rx.iter() |
each message | waits | the loop ends |
rx.try_iter() |
each queued message | ends | ends |
“Waits” is the docs’ word (“will always block the current thread”); every other cell is asserted. Whoever makes the channel holds the first tx: move it into the last worker or drop(tx) before you loop.
The other side can hang up too
Dropping the receiver makes send fail, and the value comes back in the error:
use std::sync::mpsc;
fn main() { let (tx, rx) = mpsc::channel(); tx.send(String::from("first")).unwrap(); // the receiver still exists drop(rx);
let err = tx.send(String::from("second")).unwrap_err(); println!("error: {err}"); println!("the value comes back: {:?}", err.0);
// A producer can use this to notice that nobody is listening and stop. let mut sent = 0; for i in 0..5 { if tx.send(i.to_string()).is_err() { break; } sent += 1; } println!("sent after the receiver left: {sent}");}error: sending on a closed channel the value comes back: "second" sent after the receiver left: 0
Examples write .unwrap() after a send to mean “crash if nobody is listening”; a producer usually does better to treat the error as “stop” (the third exercise). A thread that panics drops what it owned, its senders included, and so does a function that returns early:
use std::sync::mpsc;use std::thread;
fn main() { std::panic::set_hook(Box::new(|_| {})); // keep the panic message out of this demo
let (tx, rx) = mpsc::channel(); let worker = thread::spawn(move || { tx.send(1).unwrap(); tx.send(2).unwrap(); panic!("worker failed"); // unwinding drops tx });
// The loop ends, because the panicked thread's sender was dropped like any other. let received: Vec<i32> = rx.iter().collect(); println!("received before the failure: {received:?}"); println!("worker.join() returned Err: {}", worker.join().is_err());}received before the failure: [1, 2] worker.join() returned Err: true
(A panic hook silences the message. The loop ended, and join returned Err.)
| What was dropped | The other side sees |
|---|---|
one of several Senders |
nothing |
the last Sender |
queued messages first, then Err / Disconnected / the loop ends |
the Receiver |
send returns Err(SendError(value)) on every clone of the sender |
Bounded channels and back-pressure
The docs call channel() “infinitely buffered”: a producer faster than its consumer makes the queue grow without a limit. sync_channel(n) holds at most n messages: send waits while it is full, and try_send returns Full(value) instead of waiting. With 0 it is a rendezvous: each send completes only when a receiver takes the value.
use std::sync::mpsc::{self, TrySendError};
fn main() { // channel(): the queue grows. Sending never waits for the receiver. let (tx, rx) = mpsc::channel(); for i in 0..100_000 { tx.send(i).unwrap(); } println!( "channel(): 100000 sends done, {} waiting", rx.try_iter().count() );
// sync_channel(2): room for two messages. try_send says "full" instead of waiting. let (tx, rx) = mpsc::sync_channel(2); println!("try_send 1: {:?}", tx.try_send(1)); println!("try_send 2: {:?}", tx.try_send(2)); println!("try_send 3: {:?}", tx.try_send(3)); println!("recv: {:?}", rx.recv()); println!("try_send 3 again: {:?}", tx.try_send(3)); if let Err(TrySendError::Full(value)) = tx.try_send(4) { println!("full: the value {value} is handed back"); }
// sync_channel(0): a rendezvous. A send completes only when a receiver takes it. let (tx, _rx) = mpsc::sync_channel(0); println!("try_send 1 on sync_channel(0): {:?}", tx.try_send(1));}channel(): 100000 sends done, 100000 waiting try_send 1: Ok(()) try_send 2: Ok(()) try_send 3: Err(TrySendError::Full(..)) recv: Ok(1) try_send 3 again: Ok(()) full: the value 4 is handed back try_send 1 on sync_channel(0): Err(TrySendError::Full(..))
That limit is back-pressure: the producer is held to the consumer’s pace. This program asserts it at every receive, for three bounds, then shows channel() letting the producer run ahead:
use std::sync::atomic::{AtomicUsize, Ordering};use std::sync::{Arc, mpsc};use std::thread;
// How many sends have returned by the time the receiver has taken `taken` messages?// With a bound of `cap` the producer cannot be more than `cap` ahead.fn run(cap: usize) { let (tx, rx) = mpsc::sync_channel(cap); let done = Arc::new(AtomicUsize::new(0)); let producer = { let done = Arc::clone(&done); thread::spawn(move || { for i in 0..1000 { tx.send(i).unwrap(); done.fetch_add(1, Ordering::SeqCst); // this send has returned } }) }; let mut taken = 0; for _ in rx { taken += 1; assert!(done.load(Ordering::SeqCst) <= taken + cap); } producer.join().unwrap(); println!("sync_channel({cap}): never more than {cap} ahead of the receiver");}
fn main() { for cap in [0, 1, 4] { run(cap); }
// channel() has no limit: the producer finishes before anyone receives. let (tx, rx) = mpsc::channel(); thread::spawn(move || (0..1000).for_each(|i| tx.send(i).unwrap())) .join() .unwrap(); println!("channel(): producer finished 1000 sends, receiver has taken 0"); println!("then the receiver finds {}", rx.try_iter().count());}sync_channel(0): never more than 0 ahead of the receiver sync_channel(1): never more than 1 ahead of the receiver sync_channel(4): never more than 4 ahead of the receiver channel(): producer finished 1000 sends, receiver has taken 0 then the receiver finds 1000
try_send exists only on SyncSender, a different type from Sender: a function that takes one does not accept the other.
More than numbers
A message is any Send value. A channel can even carry an Arc<Mutex<..>>, and then both threads share memory again, with the lock visible in the type; Shared state says when a lock is the simpler tool. Channels are a design tool:
- an enum for several kinds of message, which also merges several sources into one receiver: one
txclone per source, the variant says who sent it; - a
Senderinside a request, so the other thread knows where to reply; - a
Result, so an error crosses to another thread as data; - a boxed closure,
Box<dyn FnOnce() + Send>, for “run this” (Closures, Trait objects).
Here one thread owns a HashMap, so no lock exists; other code asks it by message:
use std::collections::HashMap;use std::sync::mpsc::{self, Sender};use std::thread;
// Messages are an enum: the usual way to send "several kinds of thing".enum Cmd { Set(String, i32), // A request carries a Sender for the reply. Get(String, Sender<Option<i32>>), Divide(i32, Sender<Result<i32, String>>), // errors cross threads as values}
fn main() { let (tx, rx) = mpsc::channel();
// This thread owns the map. No other thread can reach it, so there is no lock. let owner = thread::spawn(move || { let mut map = HashMap::new(); for cmd in rx { match cmd { Cmd::Set(k, v) => { map.insert(k, v); } Cmd::Get(k, reply) => reply.send(map.get(&k).copied()).unwrap(), Cmd::Divide(d, reply) => { let total: i32 = map.values().sum(); let r = if d == 0 { Err("divide by zero".to_string()) } else { Ok(total / d) }; reply.send(r).unwrap(); } } } map.len() // the loop ends when every Sender is dropped });
tx.send(Cmd::Set("a".into(), 10)).unwrap(); tx.send(Cmd::Set("b".into(), 20)).unwrap();
let (reply_tx, reply_rx) = mpsc::channel(); tx.send(Cmd::Get("a".into(), reply_tx.clone())).unwrap(); println!("get a: {:?}", reply_rx.recv().unwrap()); tx.send(Cmd::Get("z".into(), reply_tx)).unwrap(); println!("get z: {:?}", reply_rx.recv().unwrap());
let (rtx, rrx) = mpsc::channel(); tx.send(Cmd::Divide(5, rtx.clone())).unwrap(); println!("sum / 5: {:?}", rrx.recv().unwrap()); tx.send(Cmd::Divide(0, rtx)).unwrap(); println!("sum / 0: {:?}", rrx.recv().unwrap());
drop(tx); println!("entries when the owner ended: {}", owner.join().unwrap());}get a: Some(10)
get z: None
sum / 5: Ok(6)
sum / 0: Err("divide by zero")
entries when the owner ended: 2A pool of workers
This is fan-out, with a cost. A Receiver cannot be cloned, so workers share one behind Arc<Mutex<..>> and take the lock only to receive the next job. Idle workers wait on the lock, not on the queue; a Java ThreadPoolExecutor or N Go goroutines on one channel need nothing like it. (crossbeam-channel has cloneable receivers; not run here.) Dropping job_tx ends the workers:
use std::sync::{Arc, Mutex, mpsc};use std::thread;
fn main() { let (job_tx, job_rx) = mpsc::channel::<u64>(); let (result_tx, result_rx) = mpsc::channel();
// A Receiver cannot be cloned (single consumer), so the workers share one behind a lock. let job_rx = Arc::new(Mutex::new(job_rx));
let workers: Vec<_> = (0..4) .map(|_| { let job_rx = Arc::clone(&job_rx); let result_tx = result_tx.clone(); thread::spawn(move || { let mut handled = 0; loop { // The guard is a temporary: it is released at the end of this statement, // so other workers can take the lock while this one handles its job. let job = job_rx.lock().unwrap().recv(); let Ok(n) = job else { break }; // closed and empty result_tx.send((n, n * n)).unwrap(); handled += 1; } handled }) }) .collect(); drop(result_tx); // workers hold the other clones
for n in 1..=20 { job_tx.send(n).unwrap(); } drop(job_tx); // no more jobs: each worker's recv will return Err
let mut results: Vec<(u64, u64)> = result_rx.iter().collect(); results.sort(); // arrival order depends on the scheduler let handled: usize = workers.into_iter().map(|w| w.join().unwrap()).sum();
println!("jobs handled by all workers: {handled}"); println!("first three results: {:?}", &results[..3]); println!( "sum of squares: {}", results.iter().map(|r| r.1).sum::<u64>() );}jobs handled by all workers: 20 first three results: [(1, 1), (2, 4), (3, 9)] sum of squares: 2870
The lock must be released before the job runs. In let job = rx.lock().unwrap().recv(); the guard is a temporary and ends at the semicolon. In while let Ok(job) = rx.lock().unwrap().recv() { .. } it lives through the loop body, so no other worker can take a job while this one is busy (try_lock returns Ok only if the lock is free):
use std::sync::{Mutex, mpsc};
fn main() { let (tx, rx) = mpsc::channel(); for job in 1..=3 { tx.send(job).unwrap(); } drop(tx); let rx = Mutex::new(rx);
// The guard is dropped at the semicolon: the lock is free while we work. let job = rx.lock().unwrap().recv().unwrap(); println!( "let: job {job}, lock free during the job: {}", rx.try_lock().is_ok() );
// In `while let` the guard lives as long as the loop body: nobody else can take a job. while let Ok(job) = rx.lock().unwrap().recv() { println!( "while let: job {job}, lock free during the job: {}", rx.try_lock().is_ok() ); }}let: job 1, lock free during the job: true while let: job 2, lock free during the job: false while let: job 3, lock free during the job: false
That compiles and gives correct answers, but the pool runs one job at a time (the Rust Book shows the same trap). The second exercise catches it.
Translating from what you know
Going deeper
What each end can do
Receiver is neither Clone nor Sync: rx.clone() is E0599 and sharing &rx between threads is E0277, which is why the pool needs a Mutex around rx. Sender is Clone, and has been Sync since Rust 1.72 (the docs say so), so a &Sender may be shared between threads. Send and Sync explains the traits.
Pipelines and closing cascades
Nobody calls close anywhere in a pipeline. Each stage loops until the stage before it drops its sender, then ends and drops its own, and the end of the input travels all the way to main:
use std::sync::mpsc;use std::thread;
fn main() { // stage 1 -> stage 2 -> main, each link a bounded channel let (num_tx, num_rx) = mpsc::sync_channel(2); let (square_tx, square_rx) = mpsc::sync_channel(2);
let generate = thread::spawn(move || { for n in 1..=10u32 { num_tx.send(n).unwrap(); } // num_tx is dropped here, so stage 2's loop will end }); let square = thread::spawn(move || { for n in num_rx { square_tx.send(n * n).unwrap(); } // square_tx is dropped here, so main's loop will end });
let squares: Vec<u32> = square_rx.iter().collect(); generate.join().unwrap(); square.join().unwrap(); println!("squares: {squares:?}"); println!("sum: {}", squares.iter().sum::<u32>());}squares: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] sum: 385
Hang-up travels backwards the same way. Here main takes three results and drops its receiver: the middle stage’s send fails, it stops, its receiver drops, and the generator’s send fails too:
use std::sync::mpsc;use std::thread;
fn main() { let (num_tx, num_rx) = mpsc::sync_channel(2); let (square_tx, square_rx) = mpsc::sync_channel(2);
let generate = thread::spawn(move || { let mut sent = 0; for n in 1..=1_000_000u32 { if num_tx.send(n).is_err() { break; // stage 2 has quit } sent += 1; } sent }); let square = thread::spawn(move || { for n in num_rx { if square_tx.send(n * n).is_err() { break; // main has quit } } });
// main takes three results and hangs up let first: Vec<u32> = square_rx.iter().take(3).collect(); drop(square_rx);
square.join().unwrap(); let sent = generate.join().unwrap(); println!("main took {first:?} and hung up"); println!("the generator stopped long before 1000000: {}", sent < 1000);}main took [1, 4, 9] and hung up the generator stopped long before 1000000: true
What std does not have
selectover several receivers. Use one receiver fed by many senders, tagged by an enum, or polltry_recvon each.crossbeam-channelprovidesselect, and async runtimes such as Tokio bring their own channels (Async basics); neither is run here.- Several consumers on one queue.
std::sync::mpmcexists but is unstable on 1.99.1 (as isoneshot):
use std::sync::mpmc;
fn main() { let (_tx, _rx) = mpmc::channel::<i32>(); // many consumers: unstable on 1.99.1}error[E0658]: use of unstable library feature `mpmc_channel` --> src/main.rs:1:5 | 1 | use std::sync::mpmc; | ^^^^^^^^^^^^^^^ | = note: see issue #126840 <https://github.com/rust-lang/rust/issues/126840> for more information error[E0658]: use of unstable library feature `mpmc_channel` --> src/main.rs:4:22 | 4 | let (_tx, _rx) = mpmc::channel::<i32>(); // many consumers: unstable on 1.99.1 | ^^^^^^^^^^^^^^^^^^^^ | = note: see issue #126840 <https://github.com/rust-lang/rust/issues/126840> for more information
- A
closecall, a sender count or a queue length:rx.len(),tx.close()andtx.sender_count()do not compile. A channel closes when you drop its ends.is_disconnectedexists but is unstable on 1.99.1 (E0658).
Practice
Try it
Replace the todo!() calls until main prints all checks passed. The starters compile and stop at the first todo!(). Checks that could hang run inside a helper that gives up after 20 seconds (it uses recv_timeout).
Results in input order
Write parallel_map: run f on every item, each on its own thread, and return the results in input order. Send (index, result) on a channel, so that arrival order does not matter. Clone the sender for each thread, and drop the original Sender: while it is alive the receiving loop never ends, for every input. Item 0 is the slowest and results fall and rise, so arrival order fails and so does sorting the values. (Joining JoinHandles in order, Java’s Futures or Python’s ThreadPoolExecutor.map also pass; this exercise is for the channel and the loop that must end.)
use std::collections::HashSet;use std::hint::black_box;use std::sync::mpsc::{self, RecvTimeoutError};use std::sync::{Arc, Mutex};use std::thread::{self, ThreadId};use std::time::Duration;
/// Runs `f` on every item, each item on its own thread, and returns the results in the/// same order as `items`.fn parallel_map<F>(items: Vec<u64>, f: F) -> Vec<u64>where F: Fn(u64) -> u64 + Send + Sync + 'static,{ todo!( "give each thread a clone of the Sender and send (index, result); drop the original Sender; put every result at its index" )}
// Gives up when `f` takes too long: a receiving loop that never ends is the likeliest bug.fn within<T: Send + 'static>(what: &str, f: impl FnOnce() -> T + Send + 'static) -> T { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let _ = tx.send(f()); }); match rx.recv_timeout(Duration::from_secs(20)) { Ok(value) => value, Err(RecvTimeoutError::Timeout) => { panic!("{what}: still waiting after 20 s. Is a Sender still alive somewhere?") } Err(RecvTimeoutError::Disconnected) => panic!("{what}: the function panicked"), }}
fn main() { let squares = within("squares", || parallel_map(vec![1, 2, 3, 4], |x| x * x)); assert_eq!(squares, [1, 4, 9, 16]);
// results that get smaller as the input grows: sorting the values is not enough let falling = within("falling", || parallel_map(vec![1, 2, 3, 4], |x| 10 - x)); assert_eq!( falling, [9, 8, 7, 6], "results must follow the input order, not their own size" );
// no items: the receiving loop must still end let none = within("no items", || parallel_map(vec![], |x| x + 1)); assert!(none.is_empty());
// Item 0 does far more work than the rest, so it finishes last. Results must still // come back in input order, run after run. for round in 0..20 { let items: Vec<u64> = (0..200).collect(); let out = within("200 items", move || { parallel_map(items, |x| { if x == 0 { let mut acc = 0u64; // black_box stops the optimizer from deleting this busy loop for i in 0..3_000_000u64 { acc = black_box(acc.wrapping_add(i)); } black_box(acc); } (x * 7919) % 1000 // neither increasing nor decreasing }) }); let expected: Vec<u64> = (0..200).map(|x| (x * 7919) % 1000).collect(); assert_eq!(out, expected, "round {round}: results are out of order"); }
// the work must not run on the calling thread let main_id = thread::current().id(); let seen: Arc<Mutex<HashSet<ThreadId>>> = Arc::new(Mutex::new(HashSet::new())); let seen2 = Arc::clone(&seen); within("threads", move || { parallel_map(vec![1, 2, 3, 4, 5, 6], move |x| { seen2.lock().unwrap().insert(thread::current().id()); x }) }); let seen = seen.lock().unwrap(); assert!(!seen.contains(&main_id), "f ran on the main thread"); assert_eq!(seen.len(), 6, "each item should get its own thread");
println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
use std::collections::HashSet;use std::hint::black_box;use std::sync::mpsc::{self, RecvTimeoutError};use std::sync::{Arc, Mutex};use std::thread::{self, ThreadId};use std::time::Duration;
/// Runs `f` on every item, each item on its own thread, and returns the results in the/// same order as `items`.fn parallel_map<F>(items: Vec<u64>, f: F) -> Vec<u64>where F: Fn(u64) -> u64 + Send + Sync + 'static,{ let count = items.len(); let f = Arc::new(f); let (tx, rx) = mpsc::channel(); for (index, item) in items.into_iter().enumerate() { let (tx, f) = (tx.clone(), Arc::clone(&f)); thread::spawn(move || tx.send((index, f(item))).unwrap()); } drop(tx); // every thread holds a clone; this is the last one that is not working
let mut results = vec![0; count]; for (index, result) in rx { results[index] = result; } results}
// Gives up when `f` takes too long: a receiving loop that never ends is the likeliest bug.fn within<T: Send + 'static>(what: &str, f: impl FnOnce() -> T + Send + 'static) -> T { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let _ = tx.send(f()); }); match rx.recv_timeout(Duration::from_secs(20)) { Ok(value) => value, Err(RecvTimeoutError::Timeout) => { panic!("{what}: still waiting after 20 s. Is a Sender still alive somewhere?") } Err(RecvTimeoutError::Disconnected) => panic!("{what}: the function panicked"), }}
fn main() { let squares = within("squares", || parallel_map(vec![1, 2, 3, 4], |x| x * x)); assert_eq!(squares, [1, 4, 9, 16]);
// results that get smaller as the input grows: sorting the values is not enough let falling = within("falling", || parallel_map(vec![1, 2, 3, 4], |x| 10 - x)); assert_eq!( falling, [9, 8, 7, 6], "results must follow the input order, not their own size" );
// no items: the receiving loop must still end let none = within("no items", || parallel_map(vec![], |x| x + 1)); assert!(none.is_empty());
// Item 0 does far more work than the rest, so it finishes last. Results must still // come back in input order, run after run. for round in 0..20 { let items: Vec<u64> = (0..200).collect(); let out = within("200 items", move || { parallel_map(items, |x| { if x == 0 { let mut acc = 0u64; // black_box stops the optimizer from deleting this busy loop for i in 0..3_000_000u64 { acc = black_box(acc.wrapping_add(i)); } black_box(acc); } (x * 7919) % 1000 // neither increasing nor decreasing }) }); let expected: Vec<u64> = (0..200).map(|x| (x * 7919) % 1000).collect(); assert_eq!(out, expected, "round {round}: results are out of order"); }
// the work must not run on the calling thread let main_id = thread::current().id(); let seen: Arc<Mutex<HashSet<ThreadId>>> = Arc::new(Mutex::new(HashSet::new())); let seen2 = Arc::clone(&seen); within("threads", move || { parallel_map(vec![1, 2, 3, 4, 5, 6], move |x| { seen2.lock().unwrap().insert(thread::current().id()); x }) }); let seen = seen.lock().unwrap(); assert!(!seen.contains(&main_id), "f ran on the main thread"); assert_eq!(seen.len(), 6, "each item should get its own thread");
println!("all checks passed");}all checks passed
A pool of boxed jobs
Finish Pool: new starts workers that share one Receiver, run sends a boxed closure, and finish lets the queued jobs run and joins the workers. jobs is an Option so that finish can take the Sender out and drop it while the workers still run. Two jobs wait for each other at a Barrier (Barrier::new(2): the first wait() sleeps until a second arrives), so they finish only if two workers are inside a job at once; a worker that keeps the lock while it runs a job is caught, as are a thread per job and a finish that does not join. finish drains the queue and cannot abort it: Go would cancel with a ctx; here you would add a shared AtomicBool that workers check (not part of this exercise).
use std::collections::HashSet;use std::sync::atomic::{AtomicUsize, Ordering};use std::sync::mpsc::{self, RecvTimeoutError};use std::sync::{Arc, Barrier, Mutex};use std::thread::{self, JoinHandle};use std::time::Duration;
// A job is any closure that can run once on another thread.type Job = Box<dyn FnOnce() + Send + 'static>;
struct Pool { // an Option so that finish() can take the Sender out and drop it while the workers still run jobs: Option<mpsc::Sender<Job>>, workers: Vec<JoinHandle<()>>,}
impl Pool { /// Starts `size` worker threads. They all take jobs from one channel. fn new(size: usize) -> Pool { todo!( "make a channel; wrap its Receiver in Arc<Mutex<..>> so every worker can share it; each worker loops: take the job (release the lock before running it!), run it, stop when recv() fails" ) }
/// Queues a job for any free worker. fn run<F: FnOnce() + Send + 'static>(&self, job: F) { todo!("box the closure and send it") }
/// Accepts no more jobs, lets the queued ones finish, and waits for the workers. fn finish(self) { todo!("drop the Sender so the workers' recv() fails, then join every worker") }}
// Gives up when `f` takes too long: a hung pool is the likeliest bug.fn within<T: Send + 'static>(what: &str, f: impl FnOnce() -> T + Send + 'static) -> T { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let _ = tx.send(f()); }); match rx.recv_timeout(Duration::from_secs(20)) { Ok(value) => value, Err(RecvTimeoutError::Timeout) => { panic!( "{what}: still waiting after 20 s. Is a lock held while waiting, or a Sender alive?" ) } Err(RecvTimeoutError::Disconnected) => panic!("{what}: the function panicked"), }}
fn main() { // every job runs exactly once, and finish() returns only after they have all run let count = Arc::new(AtomicUsize::new(0)); let counted = Arc::clone(&count); within("100 jobs", move || { let pool = Pool::new(4); for _ in 0..100 { let count = Arc::clone(&counted); pool.run(move || { count.fetch_add(1, Ordering::SeqCst); }); } pool.finish(); }); assert_eq!(count.load(Ordering::SeqCst), 100);
// no jobs at all within("no jobs", || Pool::new(3).finish());
// Two jobs that each wait for the other: they only finish if two workers are inside // a job AT THE SAME TIME. A worker that keeps the lock while it runs a job fails here. let done = Arc::new(AtomicUsize::new(0)); let done2 = Arc::clone(&done); within("two jobs at once", move || { let pool = Pool::new(2); let meet = Arc::new(Barrier::new(2)); for _ in 0..2 { let (meet, done) = (Arc::clone(&meet), Arc::clone(&done2)); pool.run(move || { meet.wait(); done.fetch_add(1, Ordering::SeqCst); }); } pool.finish(); }); assert_eq!(done.load(Ordering::SeqCst), 2);
// a pool of 3 uses 3 threads, whatever the number of jobs, and never the caller's let main_id = thread::current().id(); let ids = Arc::new(Mutex::new(HashSet::new())); let ids2 = Arc::clone(&ids); within("thread count", move || { let pool = Pool::new(3); for _ in 0..60 { let ids = Arc::clone(&ids2); pool.run(move || { ids.lock().unwrap().insert(thread::current().id()); }); } pool.finish(); }); let ids = ids.lock().unwrap(); assert!(!ids.contains(&main_id), "a job ran on the calling thread"); assert!( ids.len() <= 3, "{} threads ran jobs; the pool has 3", ids.len() );
println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
use std::collections::HashSet;use std::sync::atomic::{AtomicUsize, Ordering};use std::sync::mpsc::{self, RecvTimeoutError};use std::sync::{Arc, Barrier, Mutex};use std::thread::{self, JoinHandle};use std::time::Duration;
// A job is any closure that can run once on another thread.type Job = Box<dyn FnOnce() + Send + 'static>;
struct Pool { // an Option so that finish() can take the Sender out and drop it while the workers still run jobs: Option<mpsc::Sender<Job>>, workers: Vec<JoinHandle<()>>,}
impl Pool { /// Starts `size` worker threads. They all take jobs from one channel. fn new(size: usize) -> Pool { let (tx, rx) = mpsc::channel::<Job>(); let rx = Arc::new(Mutex::new(rx)); let workers = (0..size) .map(|_| { let rx = Arc::clone(&rx); thread::spawn(move || { loop { // the guard is a temporary: it is released before the job runs let job = rx.lock().unwrap().recv(); match job { Ok(job) => job(), Err(_) => break, // every Sender is gone and the queue is empty } } }) }) .collect(); Pool { jobs: Some(tx), workers, } }
/// Queues a job for any free worker. fn run<F: FnOnce() + Send + 'static>(&self, job: F) { self.jobs.as_ref().unwrap().send(Box::new(job)).unwrap(); }
/// Accepts no more jobs, lets the queued ones finish, and waits for the workers. fn finish(self) { drop(self.jobs); // the last Sender: recv() now fails once the queue is empty for worker in self.workers { worker.join().unwrap(); } }}
// Gives up when `f` takes too long: a hung pool is the likeliest bug.fn within<T: Send + 'static>(what: &str, f: impl FnOnce() -> T + Send + 'static) -> T { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let _ = tx.send(f()); }); match rx.recv_timeout(Duration::from_secs(20)) { Ok(value) => value, Err(RecvTimeoutError::Timeout) => { panic!( "{what}: still waiting after 20 s. Is a lock held while waiting, or a Sender alive?" ) } Err(RecvTimeoutError::Disconnected) => panic!("{what}: the function panicked"), }}
fn main() { // every job runs exactly once, and finish() returns only after they have all run let count = Arc::new(AtomicUsize::new(0)); let counted = Arc::clone(&count); within("100 jobs", move || { let pool = Pool::new(4); for _ in 0..100 { let count = Arc::clone(&counted); pool.run(move || { count.fetch_add(1, Ordering::SeqCst); }); } pool.finish(); }); assert_eq!(count.load(Ordering::SeqCst), 100);
// no jobs at all within("no jobs", || Pool::new(3).finish());
// Two jobs that each wait for the other: they only finish if two workers are inside // a job AT THE SAME TIME. A worker that keeps the lock while it runs a job fails here. let done = Arc::new(AtomicUsize::new(0)); let done2 = Arc::clone(&done); within("two jobs at once", move || { let pool = Pool::new(2); let meet = Arc::new(Barrier::new(2)); for _ in 0..2 { let (meet, done) = (Arc::clone(&meet), Arc::clone(&done2)); pool.run(move || { meet.wait(); done.fetch_add(1, Ordering::SeqCst); }); } pool.finish(); }); assert_eq!(done.load(Ordering::SeqCst), 2);
// a pool of 3 uses 3 threads, whatever the number of jobs, and never the caller's let main_id = thread::current().id(); let ids = Arc::new(Mutex::new(HashSet::new())); let ids2 = Arc::clone(&ids); within("thread count", move || { let pool = Pool::new(3); for _ in 0..60 { let ids = Arc::clone(&ids2); pool.run(move || { ids.lock().unwrap().insert(thread::current().id()); }); } pool.finish(); }); let ids = ids.lock().unwrap(); assert!(!ids.contains(&main_id), "a job ran on the calling thread"); assert!( ids.len() <= 3, "{} threads ran jobs; the pool has 3", ids.len() );
println!("all checks passed");}all checks passed
Stop when nobody is listening
Write count_up: send 0, 2, 4, … until send fails, and return how many sends succeeded and the value that bounced. A rendezvous channel makes the counts exact. A version that unwraps panics, and one that ignores the error never stops; the checks name both mistakes. This is what ctx.Done() does for a Go producer, done by the type.
use std::sync::mpsc::{self, RecvTimeoutError, SyncSender};use std::thread;use std::time::Duration;
/// Sends 0, 2, 4, ... on `tx`, one value at a time, until the receiver hangs up./// Returns how many sends succeeded, and the value that could not be delivered.fn count_up(tx: SyncSender<u64>) -> (usize, Option<u64>) { todo!( "loop over 0.. ; send(i * 2) returns Result: on Err(e) the undelivered value is e.0, so stop and return it. Do not unwrap" )}
// Gives up when `f` takes too long: a producer that ignores the error never stops.fn within<T: Send + 'static>(what: &str, f: impl FnOnce() -> T + Send + 'static) -> T { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let _ = tx.send(f()); }); match rx.recv_timeout(Duration::from_secs(20)) { Ok(value) => value, Err(RecvTimeoutError::Timeout) => { panic!("{what}: still running after 20 s. Does the loop stop when send fails?") } Err(RecvTimeoutError::Disconnected) => { panic!("{what}: the function panicked (did it unwrap a failed send?)") } }}
fn main() { // Nobody is listening from the start: nothing is sent, and the first value bounces. let (tx, rx) = mpsc::sync_channel(0); drop(rx); assert_eq!(within("no receiver", move || count_up(tx)), (0, Some(0)));
// A rendezvous channel (bound 0) hands each value straight to the receiver. After // the receiver has taken five and gone, the sixth send is the one that fails. let (tx, rx) = mpsc::sync_channel(0); let producer = thread::spawn(move || count_up(tx)); let got: Vec<u64> = rx.iter().take(5).collect(); drop(rx); assert_eq!(got, [0, 2, 4, 6, 8]); let result = within("producer", move || producer.join().unwrap()); assert_eq!(result, (5, Some(10)));
// the same with a bound of 2: the producer may be up to two values ahead let (tx, rx) = mpsc::sync_channel(2); let producer = thread::spawn(move || count_up(tx)); let got: Vec<u64> = rx.iter().take(3).collect(); drop(rx); assert_eq!(got, [0, 2, 4]); let (sent, bounced) = within("producer", move || producer.join().unwrap()); assert!((3..=5).contains(&sent), "sent {sent}"); assert_eq!(bounced, Some(2 * sent as u64));
println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
use std::sync::mpsc::{self, RecvTimeoutError, SyncSender};use std::thread;use std::time::Duration;
/// Sends 0, 2, 4, ... on `tx`, one value at a time, until the receiver hangs up./// Returns how many sends succeeded, and the value that could not be delivered.fn count_up(tx: SyncSender<u64>) -> (usize, Option<u64>) { let mut sent = 0; for i in 0.. { if let Err(e) = tx.send(i * 2) { return (sent, Some(e.0)); // the receiver is gone: stop, and say what bounced } sent += 1; } (sent, None)}
// Gives up when `f` takes too long: a producer that ignores the error never stops.fn within<T: Send + 'static>(what: &str, f: impl FnOnce() -> T + Send + 'static) -> T { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let _ = tx.send(f()); }); match rx.recv_timeout(Duration::from_secs(20)) { Ok(value) => value, Err(RecvTimeoutError::Timeout) => { panic!("{what}: still running after 20 s. Does the loop stop when send fails?") } Err(RecvTimeoutError::Disconnected) => { panic!("{what}: the function panicked (did it unwrap a failed send?)") } }}
fn main() { // Nobody is listening from the start: nothing is sent, and the first value bounces. let (tx, rx) = mpsc::sync_channel(0); drop(rx); assert_eq!(within("no receiver", move || count_up(tx)), (0, Some(0)));
// A rendezvous channel (bound 0) hands each value straight to the receiver. After // the receiver has taken five and gone, the sixth send is the one that fails. let (tx, rx) = mpsc::sync_channel(0); let producer = thread::spawn(move || count_up(tx)); let got: Vec<u64> = rx.iter().take(5).collect(); drop(rx); assert_eq!(got, [0, 2, 4, 6, 8]); let result = within("producer", move || producer.join().unwrap()); assert_eq!(result, (5, Some(10)));
// the same with a bound of 2: the producer may be up to two values ahead let (tx, rx) = mpsc::sync_channel(2); let producer = thread::spawn(move || count_up(tx)); let got: Vec<u64> = rx.iter().take(3).collect(); drop(rx); assert_eq!(got, [0, 2, 4]); let (sent, bounced) = within("producer", move || producer.join().unwrap()); assert!((3..=5).contains(&sent), "sent {sent}"); assert_eq!(bounced, Some(2 * sent as u64));
println!("all checks passed");}all checks passed
Read more
- The Rust Book, chapter 16.2: Using Message Passing to Transfer Data Between Threads, and chapter 21.2 for the thread pool, including the
while lettrap. - The standard library:
std::sync::mpsc,channel,sync_channel,Sender,SyncSender,Receiver,TryRecvError,RecvTimeoutError,SendError. - Rust by Example: Channels.
- Go’s take on the same design, “share memory by communicating”: Share Memory By Communicating.
- Errors on this page: E0382, E0277, E0599, E0658, or this site’s index.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.