Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
156: Tokio Channels: mpsc, oneshot, broadcast
I've seen this pattern countless times: a developer wants to build a "manager" task that handles requests from various parts of an application. They reach for tokio::sync::mpsc because "multi-producer, single-consumer" sounds exactly like what they need. But then they hit a wall when they realize they need the manager to actually answer the requester.
Take a look at this snippet. The goal is to have a DatabaseManager that processes queries and returns the result to the caller.
async fn database_manager(mut rx: mpsc::Receiver<String>, tx: mpsc::Sender<String>) {
while let Some(query) = rx.recv().await {
let result = format!("Result for {}", query);
// Send the result back through the channel
tx.send(result).await.unwrap();
}
}
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32);
let (resp_tx, resp_rx) = mpsc::channel(32); // Wait, this is getting messy...
tokio::spawn(database_manager(rx, tx.clone()));
tx.send("SELECT * FROM users".to_string()).await.unwrap();
// How do I get the result back specifically for THIS query?
}
The "Who is this for?" problem
The code above is fundamentally broken for a request-response pattern. First, the developer is trying to use an mpsc channel for the response. Since mpsc is single-consumer, you can't easily have ten different tasks all listening on one resp_rx to see if their specific query finished. If you try to share the receiver, you'll realize the compiler won't let you because the receiver isn't Clone.
Even if you wrapped the receiver in an Arc<Mutex<...>>, you'd have a race condition: Task A sends a query, Task B sends a query, and Task A might accidentally pop Task B's result off the channel. It's a nightmare to coordinate.
Pairing mpsc with oneshot for targeted responses
The "pro move" here is to realize that while the request is many-to-one, the response is one-to-one. This is where tokio::sync::oneshot comes in. A oneshot channel is designed for exactly one message. Instead of sending just a string through the mpsc channel, you send a tuple (or a struct) containing the request and a oneshot::Sender.
Here is how I would rewrite this to actually work:
use tokio::sync::{mpsc, oneshot};
struct Command {
query: String,
resp: oneshot::Sender<String>,
}
async fn database_manager(mut rx: mpsc::Receiver<Command>) {
while let Some(cmd) = rx.recv().await {
let result = format!("Result for {}", cmd.query);
// Send the result back specifically to the requester
let _ = cmd.resp.send(result);
}
}
#[tokio::main]
async fn main() {
let (tx, rx) = mpsc::channel(32);
tokio::spawn(database_manager(rx));
let (resp_tx, resp_rx) = oneshot::channel();
let cmd = Command {
query: "SELECT * FROM users".to_string(),
resp: resp_tx,
};
tx.send(cmd).await.unwrap();
let response = resp_rx.await.unwrap();
println!("Got response: {}", response);
}
By embedding the oneshot::Sender inside the message, the manager doesn't need to know who the caller is; it just knows that whatever is at the other end of that specific sender is waiting for the answer. This is the standard architecture for "actor-like" patterns in Rust.
Broadcasting to every listener
Now, mpsc and oneshot are great for targeted communication. But what if you need to tell everyone that the system is shutting down, or push a global configuration update? You can't use mpsc because that would require you to maintain a list of every single sender and loop through them, which is tedious and error-prone.
That's where tokio::sync::broadcast fits in. Unlike mpsc, a broadcast channel is multi-producer, multi-consumer. Every receiver gets a copy of every message sent.
use tokio::sync::broadcast;
#[tokio::main]
async fn main() {
let (tx, _) = broadcast::channel(16);
for i in 0..3 {
let mut rx = tx.subscribe();
tokio::spawn(async move {
while let Ok(msg) = rx.recv().await {
println!("Worker {} received: {}", i, msg);
}
});
}
tx.send("Global Shutdown!".to_string()).unwrap();
// Give tasks a moment to print
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
A quick warning: broadcast channels can lag. If a receiver is too slow and the internal buffer fills up, the receiver will get a RecvError::Lagged. I usually suggest keeping your broadcast messages small and your processing logic fast to avoid this.
📋 Practical Task
Exercise: Build a Multi-User Notification Hub
Your task is to build a small notification system that demonstrates all three channel types. You need to implement a Hub that manages the following:
- Private Messages: Use an
mpscchannel to send requests to the Hub. Each request should include aoneshot::Senderso the Hub can confirm the message was delivered. - Global Announcements: Use a
broadcastchannel so that any "client" subscribed to the Hub receives every global announcement.
Requirements:
- Create a
Messageenum that can either be aPrivateRequest(containing the text and a oneshot sender) or aGlobalAnnouncement(containing the text). - Implement a
hub_taskthat processes these messages. If it's a private request, it should send "Delivered!" back via the oneshot channel. If it's a global announcement, it should send that text via the broadcast channel. - In your
mainfunction, spawn two separate "client" tasks. Each client should subscribe to the broadcast channel. - The
mainfunction should send one private message (and await the confirmation) and one global announcement (which both clients should print to the console).
There are no comments for now.