Futures, Tasks, and Threads

Async programming introduces new execution units: tasks. A task is a future that the async runtime schedules, usually on a pool of operating system threads. Understanding the distinction between tasks and threads helps you design efficient systems.

Tasks vs. Threads

  • Threads are managed by the operating system. Each thread has its own stack and OS scheduling overhead.
  • Tasks are managed by the async runtime. Many tasks can run on a small number of threads.

Tasks are lightweight and great for I/O-bound work, while threads are better for heavy CPU-bound tasks unless you offload work to a dedicated thread pool.

Spawning Tasks

Most runtimes provide a task API. For example, with Tokio:

import tokio.task

async fn fetch(url: &str): String {
    // Imagine an HTTP request here
    "ok".toString()
}

async fn fetchAll(urls: Vec<String>): Vec<String> {
    let handles = urls.map { url ->
        task.spawn { -> fetch(&url) }
    }

    handles.map { handle -> await handle }
}

Each spawn call schedules a task. The runtime polls those tasks on worker threads and wakes them when they can make progress.

When to Use Threads

If you need to run blocking or CPU-heavy work, use threads (or a dedicated blocking pool) so you don't stall the async runtime:

import std.thread

fn heavyComputation(): Int {
    // CPU-intensive work
    42
}

fn runInThread(): Int {
    let handle = thread.spawn { -> heavyComputation() }
    handle.join().unwrap()
}

Async is about waiting efficiently. Threads are about doing work in parallel. Choose the tool that matches the kind of work you are doing.