Using std async and launch policies.

Async Without a Launch Policy May Never Start a Thread

I spent three years in high-frequency trading environments where “close enough” was a recipe for a catastrophic loss. I remember sitting in a freezing data center, staring at a profiler that refused to show the thread contention I knew was there, all because I had blindly relied on the default behavior of `std::async`. Most tutorials treat std async and launch policies like a magic button you press to make code “go fast,” but they fail to mention that the default policy is essentially a coin toss. If you aren’t explicitly telling the runtime how to behave, you aren’t writing concurrent code; you’re just gambling with your CPU cycles.

I’m not here to give you a lecture on the ISO standard or recite the textbook definitions you can find on cppreference. Instead, I’m going to show you how these policies actually interact with the OS scheduler and why your “asynchronous” tasks might be running synchronously when you least expect it. My goal is to strip away the abstraction and explain the mechanical reality of what happens when you call these functions, so you can stop shipping bugs that only show up under heavy load.

Table of Contents

The Stdlaunchasync vs Stdlaunchdeferred Illusion

The Stdlaunchasync vs Stdlaunchdeferred Illusion explained.

The problem is that the standard gives you a false sense of security. When you call `std::async`, you’re likely expecting a new thread to spawn immediately, but the language doesn’t actually guarantee that. If you rely on the default policy—which is `std::launch::async | std::launch::deferred`—you aren’t actually controlling your concurrency; you’re just suggesting it to the implementation.

This is where the illusion breaks. Under the default policy, the runtime is free to choose `std::launch::deferred` if it decides it’s too expensive to spawn a thread. If that happens, your task won’t run in parallel. It will sit there, dormant, until you explicitly call `.get()` or `.wait()` on the `std::future`. Suddenly, what you thought was asynchronous task execution becomes a synchronous, blocking call on your main thread.

I’ve seen this wreck production systems where developers assumed they were offloading heavy computation, only to find their latency spikes were caused by tasks executing sequentially during a result retrieval. If you want predictable behavior, stop using the default. You have to be explicit about whether you want a thread or a lazy evaluation, or you’re just gambling with your execution flow.

When Asynchronous Task Execution Becomes a Ghost

When Asynchronous Task Execution Becomes a Ghost.

The real danger isn’t just a slow program; it’s a program that looks like it’s running in parallel until it suddenly isn’t. When you rely on the default policy, you’re effectively handing the keys to the implementation’s discretion. I’ve seen systems where a heavy workload suddenly bottlenecks because the runtime decided that spawning a new thread was too expensive, opting instead for lazy evaluation. You think you’re performing asynchronous task execution, but you’re actually just building a chain of deferred function calls that only trigger when you finally call `.get()`.

This is where the abstraction leaks. If you aren’t explicitly managing your `std::async return type std::future`, you might be holding onto a handle that does nothing but sit there, consuming memory while the actual work remains trapped in a pending state. It’s a silent failure. You aren’t getting a crash or a compiler error; you’re getting stalled throughput. In high-frequency environments, that kind of unpredictability is worse than a hard failure. It’s a ghost in the machine that only appears when your latency requirements actually matter.

Rules of Engagement for std::async

  • Stop relying on the default policy. If you call `std::async` without an explicit flag, you’re handing the steering wheel to the implementation. On some compilers, it’ll spawn a thread; on others, it’ll behave like a deferred function call. If you want concurrency, force it with `std::launch::async`.
  • Respect the destructor. A common mistake is assuming `std::async` behaves like a “fire and forget” thread. It doesn’t. The future returned by `std::async` will block in its destructor if it’s the last owner, meaning your “asynchronous” task might actually turn your main thread into a synchronous bottleneck.
  • Avoid the deferred trap in latency-sensitive loops. Using `std::launch::deferred` is essentially a way to delay execution until you call `.get()` or `.wait()`. If you’re doing this inside a hot loop, you aren’t doing parallel work; you’re just adding the overhead of a state machine to your execution path.
  • Don’t use std::async for high-frequency task spawning. The overhead of thread creation and destruction in the `std::launch::async` policy is non-trivial. If you’re building something that needs to scale, you should be looking at a dedicated thread pool, not asking the OS to babysit every single small task.
  • Catch your exceptions early. If an exception is thrown inside an async task, it’s captured by the future. If you never call `.get()`, that exception is essentially swallowed until the future’s lifetime ends, at which point it might call `std::terminate`. Don’t let your errors become ghosts.

The Bottom Line

Stop assuming `std::async` is a magic wand for concurrency; without an explicit policy, you’re essentially playing Russian roulette with your execution timing.

If you rely on the default policy, your “asynchronous” code might actually be executing synchronously on the calling thread, turning your parallel logic into a serial bottleneck.

Control your execution. If you want a thread, use `std::launch::async`. If you want lazy evaluation, use `std::launch::deferred`. Anything else is just hoping the implementation behaves the way you want.

Stop Leaving Your Concurrency to Chance

The takeaway is simple: `std::async` is not a magic wand that guarantees parallelism. If you rely on the default policy, you are essentially playing Russian roulette with your thread pool, hoping the implementation chooses `async` when you actually need it. You have to be explicit. If you want a task to run on a separate thread, use `std::launch::async`. If you want to defer execution until the future is polled, use `std::launch::deferred`. Anything else is just undefined behavior in spirit, even if the standard technically allows the implementation to choose. Don’t let your performance-critical paths turn into a series of synchronous bottlenecks just because you were too lazy to pass a flag.

At the end of the day, C++ doesn’t care about your intentions; it only cares about the rules you actually follow. The language gives you the tools to build incredibly efficient, low-latency systems, but it expects you to manage the complexity rather than hiding from it. Stop treating high-level abstractions like black boxes. When you start writing code with an understanding of how the runtime actually schedules your tasks, you stop fighting the compiler and start working with it. That is where the real engineering happens.

About Ruaridh Kensington-Oyelaran

C++ rewards people who know what the compiler is allowed to do. I write about the rules that bite, the ones nobody mentions until you have already shipped the bug.

More From Author

Remove erase idiom explained with text graphic.

Remove Does Not Remove Anything, It Rearranges

Explaining double free and why it corrupts.

The Second Free Breaks the Allocator, Not Your Object