C++11
std::async
threading
parallel programming
standard library

Does standard C11 guarantee that stdasyncstdlaunchasync, func launches func in separate thread?

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 is one of the most useful concurrency tools added in C plus plus 11. The short answer is that std::async(std::launch::async, func) does guarantee asynchronous execution, but developers often misread what is guaranteed and what is implementation detail. The key is to separate the required behavior from assumptions about operating system threads.

What std::launch::async Actually Guarantees

When you pass std::launch::async, the callable is required to run in a separate thread of execution from the caller. That means the work cannot be deferred until get or wait is called, which is what std::launch::deferred allows. The function starts asynchronously, and the returned std::future represents that running task.

A practical way to observe this is to compare thread ids and timing.

cpp
1#include <future>
2#include <iostream>
3#include <thread>
4#include <chrono>
5
6int main() {
7    auto caller_id = std::this_thread::get_id();
8
9    auto fut = std::async(std::launch::async, [] {
10        std::this_thread::sleep_for(std::chrono::milliseconds(200));
11        return std::this_thread::get_id();
12    });
13
14    std::thread::id worker_id = fut.get();
15
16    std::cout << "caller id: " << caller_id << "\n";
17    std::cout << "worker id: " << worker_id << "\n";
18    std::cout << "different: " << std::boolalpha << (caller_id != worker_id) << "\n";
19}

On normal implementations you will see a different thread id. The standard guarantee is about separate execution, not the exact runtime strategy.

async Versus deferred

Many bugs come from forgetting that default launch policy is allowed to choose either async or deferred. If you omit a policy, the implementation may delay execution until get or wait, which can make code appear single threaded under load tests.

cpp
1#include <future>
2#include <iostream>
3#include <chrono>
4
5int slow_add(int a, int b) {
6    std::this_thread::sleep_for(std::chrono::milliseconds(300));
7    return a + b;
8}
9
10int main() {
11    auto start = std::chrono::steady_clock::now();
12
13    auto f1 = std::async(std::launch::deferred, slow_add, 1, 2);
14    auto f2 = std::async(std::launch::async, slow_add, 3, 4);
15
16    int r2 = f2.get();
17    int r1 = f1.get();
18
19    auto end = std::chrono::steady_clock::now();
20    auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
21
22    std::cout << "results: " << r1 << ", " << r2 << "\n";
23    std::cout << "elapsed ms: " << elapsed_ms << "\n";
24}

In this example the deferred task does not begin until f1.get. That behavior is correct and often surprising.

Lifecycle Rules That Matter in Production

A std::future from std::async is not just a value holder. It also participates in synchronization. If the task is running asynchronously and the last future referring to the shared state is destroyed before completion, destruction can block until the task finishes. This prevents detached background tasks from silently escaping lifetime management.

That blocking behavior is useful for safety, but it can also create latency spikes if futures are destroyed on critical threads.

cpp
1#include <future>
2#include <iostream>
3#include <thread>
4#include <chrono>
5
6void fire_and_forget_style_bug() {
7    std::async(std::launch::async, [] {
8        std::this_thread::sleep_for(std::chrono::seconds(1));
9        std::cout << "task done\n";
10    });
11
12    std::cout << "leaving function\n";
13}
14
15int main() {
16    auto t1 = std::chrono::steady_clock::now();
17    fire_and_forget_style_bug();
18    auto t2 = std::chrono::steady_clock::now();
19
20    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count();
21    std::cout << "function took ms: " << ms << "\n";
22}

Depending on implementation, this can pause at scope exit because the temporary future is destroyed.

Choosing the Right Pattern

Use std::launch::async when you need concurrent progress and explicit overlap with caller work. If you only need lazy evaluation, use deferred intentionally. If you need advanced pooling, cancellation, or cooperative scheduling, consider higher-level executors or task systems in your stack, since plain std::async gives limited control over queueing and thread reuse.

For reliable behavior in performance sensitive code, always specify launch policy explicitly and treat the future as part of your synchronization design.

Common Pitfalls

  • Assuming default policy always means a new thread. Fix by passing std::launch::async explicitly when concurrency is required.
  • Treating std::async as detached fire and forget. Fix by storing the returned future and managing its lifetime intentionally.
  • Calling get too early and losing parallelism. Fix by starting all tasks first, then collecting results later.
  • Ignoring exception flow from worker code. Fix by handling exceptions around future.get, since worker exceptions are rethrown there.
  • Expecting fine grained scheduler control. Fix by using a custom executor framework when you need priority, throttling, or affinity.

Summary

  • std::launch::async guarantees asynchronous execution on a separate thread of execution.
  • Default launch policy may choose deferred, so behavior can vary if policy is omitted.
  • std::future lifetime affects blocking and correctness.
  • Explicit policy plus explicit result collection gives predictable concurrency.
  • Treat std::async as a high level convenience, not a full scheduler.

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

All Rights Reserved.