Concurrency
Multithreading
C++ Programming
std::async
Parallel Computing

Control degree of parallelism with stdasync

Master System Design with Codemia

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

Introduction

std::async can start work asynchronously, but it does not give you a built-in knob for "run at most N tasks in parallel." That is the key point. If you need explicit degree-of-parallelism control, you usually have to add your own scheduling layer or use a thread pool instead of relying on raw std::async calls alone.

What std::async Actually Controls

std::async mainly controls launch policy, not concurrency limits.

The launch options are:

  • 'std::launch::async, which asks for asynchronous execution'
  • 'std::launch::deferred, which delays execution until you wait on the future'
  • the default combined policy, where the implementation may choose either

None of these says "use exactly four workers" or "limit in-flight tasks to eight."

A Simple In-Flight Limit Pattern

One practical workaround is to keep only a fixed number of futures active at once. When the limit is reached, wait for one task to finish before launching another.

cpp
1#include <future>
2#include <iostream>
3#include <thread>
4#include <vector>
5
6int work(int value) {
7    std::this_thread::sleep_for(std::chrono::milliseconds(100));
8    return value * value;
9}
10
11int main() {
12    const std::size_t max_parallel = 3;
13    std::vector<std::future<int>> active;
14    std::vector<int> results;
15
16    for (int i = 1; i <= 8; ++i) {
17        active.push_back(std::async(std::launch::async, work, i));
18
19        if (active.size() == max_parallel) {
20            results.push_back(active.front().get());
21            active.erase(active.begin());
22        }
23    }
24
25    for (auto& fut : active) {
26        results.push_back(fut.get());
27    }
28
29    for (int r : results) {
30        std::cout << r << '\n';
31    }
32}

This is not a true executor framework, but it does bound the number of concurrently active async tasks from the caller's point of view.

Why This Is Only a Partial Solution

The example above limits how many futures you keep in flight, but it still depends on std::async for the actual scheduling model. That means:

  • thread creation cost can still be significant
  • behavior may vary by implementation
  • task ordering and queueing are still primitive

For small tools, this can be good enough. For serious workloads, a thread pool or task scheduler is usually the better abstraction.

When a Thread Pool Is Better

If you truly care about degree of parallelism, a thread pool is often the correct tool. A fixed-size pool gives you:

  • explicit worker count
  • predictable task queueing
  • less thread creation overhead
  • clearer control of throughput

std::async is convenient for fire-and-forget style futures, but it is not a general-purpose concurrency governor.

Do Not Rely on the Default Policy

Many developers call std::async without specifying a launch policy and then assume they are getting real parallelism. That is unsafe.

With the default policy, the implementation is allowed to defer execution until you call get() or wait(). If you care about actual asynchronous execution, specify std::launch::async explicitly.

Even then, explicit degree-of-parallelism control is still your responsibility.

Common Pitfalls

The most common mistake is assuming std::async has a built-in "maximum threads" parameter. It does not.

Another mistake is relying on the default launch policy and then wondering why work did not actually run in parallel.

A third pitfall is using std::async for a large number of tiny tasks where thread-pool scheduling would be more efficient.

That pattern often hurts throughput instead of helping it.

Summary

  • 'std::async manages task launch policy, not explicit degree of parallelism.'
  • To limit concurrency, you need your own in-flight task cap or a thread pool.
  • Use std::launch::async when you require actual asynchronous execution.
  • For small programs, a bounded-futures pattern can be enough.
  • For real workload control, prefer an executor or fixed-size worker pool design.

Course illustration
Course illustration

All Rights Reserved.