tokio::spawn
Tokio is an asynchronous runtime for Rust. “It provides the building blocks needed for writing network applications.”
The #[tokio::main] function is a macro. It transforms the async fn main() into a synchronous fn main() that initializes a runtime instance and executes the async main function. ~ https://tokio.rs/tokio/tutorial/hello-tokio
#[tokio::main]
async fn main() {
println!("hello");
}
gets transformed into:
fn main() {
let mut rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
println!("hello");
})
}
tokio::spawn – thread pool
What Happens Without task.await.unwrap()
?
1. Task Runs Independently (Fire-and-Forget)
By default, a task spawned with tokio::spawn
runs concurrently and independently of the main task. Without awaiting it, the spawned task starts execution, but the main function does not wait for it to finish.
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
tokio::spawn(async {
sleep(Duration::from_secs(1)).await;
println!("Task completed!");
});
println!("Main function ends");
}
Output:
Main function ends
Task completed!
In this example, the main function ends before the spawned task finishes, but the task continues running in the background until it completes.
2. Why Use await
?
By calling task.await
, the main task explicitly waits for the spawned task to complete. This is useful when:
- You need to ensure order: Ensure that the spawned task completes before proceeding.
- Error handling:
task.await
returns aResult
that allows you to handle errors from the spawned task.
Example with await
:
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let task = tokio::spawn(async {
sleep(Duration::from_secs(1)).await;
println!("Task completed!");
42 // Return a value from the task
});
let result = task.await.unwrap();
println!("Task result: {}", result);
}
Output:
Task completed!
Task result: 42
3. Without await
, Potential Issues:
- Unfinished Tasks: The program might terminate before background tasks complete, especially in short-lived programs.
- Unobservable Errors: Errors in the spawned task are silently ignored unless awaited.
When Skipping await
Is Okay:
- Fire-and-forget tasks: If the task performs work you don’t need to track, such as logging or background cleanup.
- Long-running tasks: Tasks designed to run for the program’s lifetime (e.g., a server handling requests).
Does tokio::spawn
Spawn a New Thread?
No, tokio::spawn
does not spawn a new OS thread. Instead, it schedules the asynchronous task to be run on the Tokio runtime’s thread pool. Here’s how it works:
1. How tokio::spawn
Works
When you call tokio::spawn
, the following happens:
- Task Creation: The provided async block or function is wrapped into a task, which is an object that implements the
Future
trait. - Scheduling: This task is handed over to the Tokio runtime, which determines when and on which worker thread the task should run.
- Execution: The runtime executes the task on one of the threads in its thread pool. Tasks may be paused and resumed depending on whether they are waiting for I/O or other asynchronous operations.
This means that tasks created with tokio::spawn
are lightweight and cooperative:
- They share threads with other tasks.
- They yield control when performing asynchronous operations, allowing other tasks to run.
2. Does It Use a New Thread?
No, tokio::spawn
does not create a new thread for every task. Instead:
- In the multi-threaded runtime (default), tasks run on a shared thread pool that typically matches the number of CPU cores.
- In the current-thread runtime, all tasks run on a single thread (no thread pool).
Example of thread reuse: If you spawn 1,000 tasks, they will all share the same thread pool (multi-threaded runtime) or single thread (current-thread runtime), not 1,000 threads.
3. Why Is This Efficient?
- Lightweight Tasks: Unlike OS threads, tasks in Tokio are cheap to create and manage.
- Asynchronous Execution: Tasks only consume CPU when they’re actively performing work. They yield when waiting for I/O, timers, or other async operations, allowing other tasks to use the same thread.
- Thread Pool: A fixed number of threads (by default,
num_cpus::get()
threads) handle many tasks, avoiding the overhead of thread creation and destruction.
4. Compare to OS Threads
Aspect | tokio::spawn Task | OS Thread |
---|---|---|
Cost | Very lightweight | Expensive (OS-managed) |
Concurrency | Cooperative (tasks yield control) | Preemptive |
Thread Creation | Does not create a new thread | Creates a new thread |
5. Example with Thread IDs
To demonstrate that tasks share threads, you can log the thread ID for each task:
use tokio::task;
use std::thread;
#[tokio::main]
async fn main() {
for _ in 0..5 {
tokio::spawn(async {
println!("Running on thread: {:?}", thread::current().id());
});
}
}
Output:
Running on thread: ThreadId(1)
Running on thread: ThreadId(2)
Running on thread: ThreadId(1)
Running on thread: ThreadId(2)
Running on thread: ThreadId(1)
This shows that tasks reuse existing threads rather than creating new ones for every tokio::spawn
.
Conclusion
tokio::spawn
schedules tasks to run on the Tokio runtime’s thread pool. It does not create a new thread for each task, making it efficient for handling thousands or even millions of concurrent tasks. If you need explicit thread creation, you can use Rust’s standard library (std::thread
) instead.