std::async is the shortest path from "I have a function" to "run it in parallel and give me the result back". You hand it a callable, you get a std::future, and the standard library handles the thread plumbing. The previous chapter covered std::future and std::promise as the building blocks. This chapter covers the high-level launcher that wires them up for you, the launch policies that decide whether a new thread is actually spawned, and the destructor gotcha that has cost more debugging hours than it should have.
std::async FirstA raw std::thread solves "run this in the background", but it gives you no easy way to get a value back, and any exception thrown by the work function calls std::terminate. To return a result, you have to wire up a std::promise and a std::future by hand, capture the promise into the lambda, and remember to set the value or exception on every path. That works, but it's a lot of boilerplate for "compute X off the main thread and give me X".
std::async collapses all of that into one call. You pass a callable, you get back a std::future<T> where T is whatever the callable returns. The implementation arranges for the function's return value to flow into the future, and any exception thrown by the function flows into the future too. Calling get() either returns the value or rethrows the exception.
Compare that to the raw std::thread + std::promise version of the same idea, and is written std::async from now on for any task that returns a single value. The trade-offs come later: when you need fine control over thread lifetime, when you want to reuse threads from a pool, when the default launch policy is too vague. .
std::async lives in <future>. Its signature, simplified to the shape is used, looks like this:
You pass a launch policy (covered those next), a callable, and the arguments to forward to the callable. The return type is a std::future parameterized on whatever the callable returns. The arguments are copied or moved into the call by default, exactly like std::thread's constructor. If you want a reference, wrap the argument in std::ref or std::cref.
Without the std::ref, the function would modify a local copy of list and the original list in main would stay empty. This is the same lesson as the threads chapter, but it shows up here often enough to repeat once.
The callable can be a free function, a lambda, a functor, or a pointer to a member function. For member functions, the first argument after the policy is the object (or pointer to the object) you're calling on.
That covers the call shape. The interesting part is the policy.
The first argument to std::async is a std::launch enum that tells the standard library how to schedule the work. There are three useful values, and the difference between them matters a lot.
| Policy | What Happens | When get() Blocks |
|---|---|---|
std::launch::async | The function runs on a new thread, immediately. | Until the thread finishes. |
std::launch::deferred | The function does not run until you call get() or wait(). It then runs on the calling thread. | Until the deferred call returns. |
std::launch::async | std::launch::deferred (default) | Implementation chooses either. | Depends on what the implementation chose. |
The flags are bitmasks, so you can pass std::launch::async | std::launch::deferred to mean "either is fine". That's also what you get if you omit the policy argument entirely. Letting the implementation choose sounds friendly, but it makes the program's behavior implementation-defined. We'll come back to why that matters.
The diagram lays out the three branches. async always means a new thread. deferred always means lazy, on the caller. The default mode is the trapdoor: the same code can take either branch on different builds, machines, or load conditions.
std::launch::async: Eager, on a New ThreadWith std::launch::async, the runtime spawns a thread and starts running your function before std::async returns. By the time you get the future back, the work is already in flight on another core, assuming one is free.
The total time is around 200ms, not 300ms. The 100ms of "other work" in main ran in parallel with the 200ms of slowQuote on the background thread. get() then waited the remaining 100ms for the function to finish. If you swap to std::launch::deferred and re-run, the elapsed time is roughly 300ms instead, because slowQuote doesn't start until get() is called.
Each std::async(std::launch::async, ...) call typically creates a fresh OS thread. On Linux, that's a pthread_create syscall plus kernel scheduling overhead, often a few hundred microseconds per call. Fine for a handful of long-running tasks. Not fine for thousands of short ones.
std::launch::deferred: Lazy, on the Callerstd::launch::deferred does not start the function when you call std::async. It packages the callable and arguments and returns a future. The function only runs when something asks for its result, either through get() or wait(). At that point, the function executes synchronously on the thread that called get(). No thread is created.
The thread ID inside computeRecommendations is identical to the main thread. There's no parallelism at all in deferred mode. Why is it useful? Two scenarios.
First, when the work might not be needed. If a code path computes a value that might be consumed by some callers and ignored by others, deferring lets you skip the work entirely when nobody calls get(). The price you pay is a small amount of bookkeeping. The function never runs.
Second, when you want a uniform "future-returning" interface but don't want the overhead of a thread for cheap tasks. A function that returns a std::future<T> looks the same to callers whether the work was async or deferred, and the caller waits on get() the same way.
There's a subtle behavior to be aware of. fut.wait_for(std::chrono::seconds(0)) on a deferred future does not report ready. It reports std::future_status::deferred. That's the only way to ask "is this future actually going to run on its own?", and this is revisited in the pitfalls section.
async | deferredIf you call std::async(f) without specifying a policy, you get the default mask std::launch::async | std::launch::deferred. The standard says the implementation picks one. It does not say how. In practice, libstdc++ and libc++ both lean toward async when threads are available and may fall back to deferred under heavy load. The behavior is not portable, and worse, it's not even predictable within a single build under different system conditions.
This compiles and runs fine, but you have no idea whether the lambda ran on a new thread or on the main thread when get() was called. For a single quick task that doesn't matter. For a fan-out of N parallel quotes where you assumed each one runs concurrently, it can turn into sequential execution on a stressed system, ruining the parallelism you thought you had.
The default async | deferred policy is implementation-defined. If you wrote the code expecting parallelism, always pass std::launch::async explicitly. If you wrote it expecting laziness, pass std::launch::deferred. Never rely on the default for anything performance-sensitive.
The most common use of std::async is fan-out: you have N independent tasks, you fire them all off, and then you collect the results. For an e-commerce shipping page, that maps directly onto "ask FedEx, UPS, USPS, and DHL for a quote and show the cheapest one".
Four 200ms calls that run sequentially would take 800ms. Running them in parallel, the total is roughly the slowest single call, around 200ms. That's the whole win of fan-out, and std::async is the cleanest way to express it.
A few details in the code above matter. The futures are stored in a std::vector<std::future<Quote>>. Each future is move-only (futures cannot be copied), and vector::push_back of a move-only type works fine because the future is constructed in place. We collect all the futures first, then iterate to call get() on each. If you instead called get() inside the launching loop, you would block on the first quote before launching the second, and you would have lost the parallelism.
Launching N async tasks creates up to N threads. On a 4-core machine, launching 1000 quote-fetch tasks at once is oversubscription: the OS will schedule them, but context switching will dominate and your wall-clock time will get worse, not better. For "many short tasks", use a thread pool, covered later in this section.
Here is the part of std::async that surprises people. The future returned by std::async(std::launch::async, ...) has a special destructor that blocks until the associated task finishes. No other std::future does this. A future from std::promise::get_future() or std::packaged_task::get_future() destroys without waiting. The future from std::async doesn't.
The reason is safety. If std::async launched a thread and you dropped the future on the floor, the thread would still be running, holding references to dead local variables. The standard chose "block in the destructor" as the lesser of two evils. The result is that this innocuous-looking code is sequential:
The total is 400ms, not 200ms, even though you wrote what looks like a parallel fan-out. The futures returned from std::async are temporaries, and at the end of each full expression (each statement here), the temporary is destroyed. The destructor of an async-launched future blocks until the task finishes. So the first call blocks for 200ms, then the second runs, blocks for another 200ms, then main continues.
The fix is to keep the future alive past the launch site, so it doesn't get destroyed at the end of the statement:
Now both tasks run in parallel and the total is about 200ms. The two futures are local variables that live until the end of main, so neither destructor runs early.
The diagram contrasts the two paths. The top path is the trap: a temporary future whose destructor blocks at the end of the statement, serializing what should have been parallel work. The bottom path is the correct one: capture the future in a named variable so it stays alive while parallel work continues.
A second symptom of this gotcha shows up when you push futures into a container in a loop without naming them first, but the temporary actually lives until the vector takes ownership of it via the move constructor, so the issue here is different. The pure case to remember is "calling std::async without binding the result to a name".
There is no clean way to "detach" a future from its task. If you want truly fire-and-forget, the standard answer is to use std::thread with .detach(), not std::async. The std::async model assumes you want the result, and the destructor enforces that you don't drop the work .
Exceptions thrown inside the async function don't crash the program. They get captured by the future. The next call to get() rethrows the exception in the caller's thread. This mirrors how std::future and std::promise handle exceptions, just with the wiring done for you.
The exception traveled across the thread boundary. The async worker captured it into the shared state; get() saw the captured exception and rethrew it in the main thread.
This works the same for std::launch::deferred. The function runs on the caller's thread when get() is called, and any exception it throws propagates straight out of get(). No magic, no double-wrapping.
One detail to remember: get() can only be called once per future. After it returns (or throws), the future is empty and the shared state is gone. If you need to consume the result from multiple call sites, convert to a std::shared_future with fut.share(). Multiple shared_futures referring to the same shared state can all call get() independently, and each gets the same value or the same exception.
std::async is one of three reasonable answers to "I want to run this off the main thread". The other two are raw std::thread and a thread pool. A rough guide.
| Tool | Use When | Skip When |
|---|---|---|
std::async | You want a future back. You have a handful of independent tasks. You don't care which exact thread runs the work. | You're launching thousands of tasks. You need control over thread lifetime. You need fire-and-forget. |
std::thread | You need to start a long-running background loop. You want to detach. You don't need a return value. | You need to compose tasks that return values. You'd be writing your own promise/future plumbing. |
| Thread pool | You have many short tasks and want bounded concurrency. You want to reuse threads. | A small, fixed number of independent tasks where the simplicity of std::async wins. |
The diagram walks the decision. "Do I want a result back?" is the first branching question. If yes and the number of tasks is small, std::async wins on clarity. If yes and the number is large, the cost of one thread per task becomes prohibitive and a pool is the right call. If you don't need a result and you want explicit lifetime control (detach, named thread, custom stack), std::thread is still the right primitive.
A practical sketch of when std::async is a suitable choice: an order placement flow that needs to ask three independent services (inventory, payment authorization, shipping quote) in parallel and combine their answers. Three tasks, each returning a value, each independent. That's a clean fan-out and std::async is the simplest way to write it.
Three independent checks, three futures, one place where the results are joined. The code reads top to bottom and the parallelism is obvious. No mutexes, no condition variables, no std::promise boilerplate. That's the case std::async was built for.
std::packaged_task Fits InYou might wonder how std::async works internally. The short answer: std::async is conceptually std::packaged_task plus a thread plus an implementation-defined scheduling decision. std::packaged_task wraps a callable so its return value (or exception) flows into a std::future, exactly like std::async, but it doesn't decide where the task runs. You decide: you can pass it to a std::thread, you can keep it on a queue, you can run it inline.
std::async is the easy mode that picks the scheduling for you. std::packaged_task is the building block you use when you want to separate "wrap the work into something future-returning" from "run it on the thread of my choice", typically because you have a thread pool. The next chapter covers std::packaged_task directly.
10 quizzes