In this lesson, you will learn why Send and Sync bounds are the bridge between your asynchronous tasks and the multi-threaded executors that run them.
Why Executors Need Send
In most modern Rust applications, we use multi-threaded executors (like Tokio’s default runtime). These executors use a work-stealing strategy: when one thread finishes its queue, it "steals" pending tasks from other threads.
For a task to be stolen and moved to a different thread, the Future representing that task must be Send. If a Future contains a non-Send type—such as Rc<T> or a RefCell<T>—the compiler will prevent you from spawning it onto a multi-threaded runtime. If it weren't for this check, moving a Future across threads could trigger a data race, as those types are explicitly designed for single-threaded usage.
The Impact of .await Points
When you use await, the compiler generates a state machine representing your Future. Crucially, this state machine captures every variable that is "live" across an await point.
Consider this example:
async fn process_data(data: Rc<String>) {
// Some work...
do_something().await; // The state machine is suspended here
println!("{}", data);
}
Because Rc<String> is held across the await point, the entire Future state machine becomes !Send. Even if you aren't manually sending data, the compiler detects that the Future contains a type that cannot safely move between threads. To fix this, you would need to use Arc<T> instead of Rc<T>, which is specifically designed to be Send and Sync across thread boundaries.
Enforcing Bounds
If you are writing a library that accepts Futures, you can enforce these requirements in your bounds:
fn spawn_task<F>(future: F)
where
F: Future<Output = ()> + Send + 'static
{
// ...
}
By requiring Send, you guarantee that your executor can safely migrate the task between its worker threads. By requiring 'static, you ensure the task doesn't rely on short-lived references, preventing dangling pointers when a task is moved to a background thread.
Key Takeaway: The Send bound on a Future is a safety guarantee that ensures the task’s captured state—held across await points—can be safely migrated between threads by the executor.