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

When to use stdasync vs stdthreads?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

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.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions