std::async
std::threads
C++
concurrency
multithreading

When to use stdasync vs stdthreads?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

std::async and std::thread both let C++ code run work concurrently, but they solve different problems. std::thread gives direct control over a thread of execution, while std::async is a task-oriented abstraction that couples background work with a std::future result.

Use std::async for result-producing tasks

If the main goal is "run this function and give me the result later," std::async is usually the cleaner tool. It handles result transport and exception propagation without requiring you to build that plumbing yourself.

cpp
1#include <future>
2#include <iostream>
3#include <vector>
4
5int sum_range(const std::vector<int>& values) {
6    int total = 0;
7    for (int value : values) {
8        total += value;
9    }
10    return total;
11}
12
13int main() {
14    std::vector<int> values = {1, 2, 3, 4, 5};
15
16    auto future = std::async(std::launch::async, sum_range, values);
17
18    std::cout << "main thread keeps working\n";
19    std::cout << "sum = " << future.get() << "\n";
20}

The important part is future.get(). It waits for completion, returns the value, and rethrows any exception that happened in the async task. That makes std::async a good default for one-off computations.

Use std::thread when you need thread ownership and lifecycle control

Choose std::thread when the thread itself matters. Typical examples are long-lived workers, background loops, custom synchronization, CPU affinity, or integrating with lower-level threading APIs.

cpp
1#include <chrono>
2#include <iostream>
3#include <thread>
4#include <atomic>
5
6void worker(std::atomic<bool>& running) {
7    while (running.load()) {
8        std::cout << "worker heartbeat\n";
9        std::this_thread::sleep_for(std::chrono::milliseconds(200));
10    }
11}
12
13int main() {
14    std::atomic<bool> running{true};
15    std::thread background(worker, std::ref(running));
16
17    std::this_thread::sleep_for(std::chrono::seconds(1));
18    running.store(false);
19    background.join();
20}

Here the program owns the thread directly. That matters because the worker has a lifecycle independent of any single return value.

std::async is higher-level, but launch policy matters

A common mistake is forgetting that std::async can run with deferred execution unless you request std::launch::async. Deferred execution means the function may not run on another thread at all; it can wait until get() or wait() is called.

cpp
auto future = std::async(std::launch::async, heavy_function);

If you need guaranteed background execution, specify the launch policy explicitly. If you are fine with implementation-controlled behavior, the default can be acceptable, but it becomes harder to reason about timing.

Exception handling is much easier with std::async

With std::thread, uncaught exceptions inside the thread function call std::terminate. That means you must catch exceptions in the thread and move the error back manually.

cpp
1#include <exception>
2#include <future>
3#include <iostream>
4#include <thread>
5
6void work(std::promise<int> promise) {
7    try {
8        throw std::runtime_error("failure in worker");
9    } catch (...) {
10        promise.set_exception(std::current_exception());
11    }
12}
13
14int main() {
15    std::promise<int> promise;
16    auto future = promise.get_future();
17    std::thread t(work, std::move(promise));
18
19    try {
20        future.get();
21    } catch (const std::exception& ex) {
22        std::cout << ex.what() << "\n";
23    }
24
25    t.join();
26}

If you find yourself writing this pattern often, std::async is probably the better abstraction.

Performance and pooling considerations

Neither abstraction is a full task scheduler. std::thread usually creates a dedicated thread immediately. std::async may create a thread or may defer, depending on policy and implementation. If the application needs a bounded worker pool or a large number of short tasks, neither one is ideal by itself. A thread pool or executor abstraction is often a better fit.

The practical rule is simple: std::async is great for a small number of result-oriented tasks, and std::thread is better when you need explicit concurrency architecture.

Common Pitfalls

  • Using std::thread for simple background computations and then re-implementing futures with promise and future.
  • Forgetting to join() or detach() a std::thread, which causes program termination.
  • Assuming std::async always starts a new thread even when no launch policy is specified.
  • Ignoring exception propagation differences between std::async and std::thread.
  • Treating either abstraction as a substitute for a real thread pool in high-task-count workloads.

Summary

  • Use std::async when you want to run a function and collect its result later.
  • Use std::thread when you need direct control over thread lifetime and behavior.
  • Specify std::launch::async if background execution must happen immediately.
  • Prefer std::async when exception propagation and returned values matter.
  • Reach for a thread pool when the workload consists of many short tasks.

Course illustration
Course illustration

All Rights Reserved.