Skip to content

Q044. How does an OS thread differ from an async task?

Short Answer

An OS thread is scheduled preemptively by the operating system and has its own stack. An async task is a language/runtime object, usually a heap-allocated future, scheduled cooperatively by an executor. Tasks are much cheaper to create and suspend, so one process can run many of them, but a task must reach .await or otherwise yield for another task on the same worker to run.

Deep Dive

Threads are appropriate for CPU parallelism, blocking APIs, thread-affine libraries, and workloads that need preemptive scheduling. Their stacks and kernel scheduling make them heavier than tasks.

Async tasks are effective for large numbers of mostly waiting operations. Suspending a task preserves state in its future instead of blocking an entire OS thread. The executor can then use a small worker-thread pool to drive many connections.

A task is not independent of threads: some OS thread must poll it. On a multi-thread executor, a Send task may resume on a different worker after an .await; it does not have permanent thread affinity.

Internal Model

The kernel stores thread scheduling state, registers, and stack metadata, and can interrupt a running thread. An executor stores task state, readiness metadata, and a future. The future's generated state machine contains local values that survive suspension.

Task switching usually happens in user space when polling returns Pending; thread context switching involves the OS scheduler.

Example

rust
#[tokio::main]
async fn main() {
    let mut tasks = Vec::new();

    for id in 0..100 {
        tasks.push(tokio::spawn(async move { id * 2 }));
    }

    let mut sum = 0;
    for task in tasks {
        sum += task.await.unwrap();
    }

    assert_eq!(sum, 9_900);
}

Common Mistakes

  • Saying each async task has a dedicated OS thread.
  • Assuming a cooperative executor can preempt any long-running task.
  • Relying on a Tokio task to resume on the same worker thread.

Follow-ups

  • Why must a multi-threaded executor usually require spawned futures to be Send?
  • What state is stored when an async task is suspended?
  • When is a current-thread runtime useful?

References

  • Async Book
  • Tokio Documentation
  • Linux kernel documentation