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 a Result 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

Aspecttokio::spawn TaskOS Thread
CostVery lightweightExpensive (OS-managed)
ConcurrencyCooperative (tasks yield control)Preemptive
Thread CreationDoes not create a new threadCreates 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.