tokio::spawn
Tokio is an asynchronous runtime for Rust. “It provides the building blocks needed for writing network applications.” – It’s pretty much the de-facto standard for Rust!
Before we look at “spawn” let’s review what tokio is, how to use it, and look at what tasks are.
tokio = { version = "1", features = ["full"] }
tokio::main
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");
})
}
Tasks
Asynchronous programs in Rust are based around lightweight, non-blocking units of execution called tasks
Below is the Cargo.toml adjusted to import features for a library that just needs to tokio::spawn
and use a TcpStream
tokio = { version = "1", features = ["rt", "net"] }
Tasks
what are tasks?
https://docs.rs/tokio/latest/tokio/task/index.html#what-are-tasks
A task is a light weight, non-blocking unit of execution. A task is similar to an OS thread, but rather than being managed by the OS scheduler, they are managed by the Tokio runtime. Another name for this general pattern is green threads.
1. Task Runs Independently (Fire-and-Forget) – tokio::spawn
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).
How await
Works
await
: “Keep doing stuff until the result is available. When it’s available, resume from here.”It pauses the current task (but not the entire thread) and lets the Tokio runtime perform other tasks.
It’s efficient because it prevents unnecessary blocking while waiting for I/O operations, timers, etc.
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.
tokio::spawn
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
.
https://docs.rs/tokio/latest/tokio/#examples
Multi-threaded runtime
If you use #[tokio::main]
without specifying the flavor
, it defaults to the multi-threaded runtime. This means:
- Multiple Threads: Tokio creates a thread pool, with the number of threads determined by the number of CPU cores (you can customize this with
worker_threads
). - Task Scheduling: Tasks created with
tokio::spawn
can run on any thread in the thread pool, so you’re likely to see different thread IDs for tasks and the main function.
Example:
use tokio::task;
use std::thread;
#[tokio::main] // Defaults to multi-threaded runtime
async fn main() {
println!("Main thread: {:?}", thread::current().id());
tokio::spawn(async {
println!("Spawned task thread: {:?}", thread::current().id());
})
.await
.unwrap();
}
Possible Output:
Main thread: ThreadId(1)
Spawned task thread: ThreadId(2)
In this example:
- The main task runs on one thread (e.g.,
ThreadId(1)
). - The spawned task runs on a different thread from the thread pool (e.g.,
ThreadId(2)
).
Runtime Behavior:
Single-threaded runtime (current_thread
):
#[tokio::main(flavor = "current_thread")]
async fn main() {
// Single-threaded runtime: All tasks use the same thread.
}
Multi-threaded runtime (multi_thread
):
#[tokio::main(flavor = "multi_thread", worker_threads = 4)] // Specify threads if needed
async fn main() {
// Multi-threaded runtime: Tasks can run on different threads in the pool.
}
Default behavior (#[tokio::main]
):
Equivalent to:
#[tokio::main(flavor = "multi_thread")]
By default, #[tokio::main]
gives you the multi-threaded runtime, so tasks are distributed across multiple threads.
Summary:
#[tokio::main]
defaults to the multi-threaded runtime.- Tasks can run on different threads, managed by Tokio’s thread pool.
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.