Advanced 12 min

Async

Simplify asynchronous programming with std::async for automatic thread management and result handling

Learn how to run functions asynchronously with std::async, the high-level interface for parallel execution without manual thread management.

A Simple Example

#include <iostream>
#include <future>
#include <chrono>
#include <vector>

int computeExpensiveTask(int id) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Task " << id << " completed\n";
    return id * id;
}

int main() {
    std::cout << "Launching async tasks...\n";

    // Launch multiple async tasks
    std::vector<std::future<int>> futures;
    futures.push_back(std::async(std::launch::async, computeExpensiveTask, 1));
    futures.push_back(std::async(std::launch::async, computeExpensiveTask, 2));
    futures.push_back(std::async(std::launch::async, computeExpensiveTask, 3));

    std::cout << "All tasks launched, doing other work...\n";

    // Simulate other work
    std::this_thread::sleep_for(std::chrono::milliseconds(500));

    // Collect results
    std::cout << "\nCollecting results:\n";
    for (auto& future : futures) {
        int result{future.get()};  // Blocks until result is ready
        std::cout << "Got result: " << result << "\n";
    }

    // Deferred execution example
    std::cout << "\nDeferred execution:\n";
    auto deferredFuture{std::async(std::launch::deferred, computeExpensiveTask, 4)};
    std::cout << "Task not started yet...\n";
    int result{deferredFuture.get()};  // Executes NOW, synchronously
    std::cout << "Deferred result: " << result << "\n";

    return 0;
}

Breaking It Down

std::async basics

  • What it does: Runs a function asynchronously and returns a future
  • Syntax: std::async(policy, function, args...)
  • Returns: std::future that will hold the result
  • Remember: Much simpler than manually creating threads and promises

Launch policies

  • std::launch::async: Guaranteed to run on a new thread immediately
  • std::launch::deferred: Lazy evaluation - runs when you call get()
  • Default: Implementation chooses async or deferred
  • Remember: Use async for true parallelism, deferred for lazy evaluation

Exception handling

  • Automatic propagation: Exceptions are captured in the future
  • Re-thrown on get(): When you call get(), exceptions are re-thrown
  • No manual handling: Unlike threads, no need for try-catch in async function
  • Remember: Handle exceptions when calling get() on the future

When to use std::async

  • Use for: Task-based parallelism, independent computations
  • Avoid for: Fine-grained control, thread pools, continuous work
  • Benefit: Clean syntax, automatic lifetime management
  • Remember: Great for simple async tasks, not complex threading patterns

Why This Matters

  • std::async abstracts away thread management, making parallel programming simpler and less error-prone.
  • Automatic future management means you get results cleanly without manually handling promises and threads.
  • Launch policies let you control whether execution is truly async or deferred until needed.

Critical Insight

std::async is like hiring contractors for specific jobs. You tell them what to do (the function), they do it in parallel (on their own thread), and they hand you the result (the future) when they are done.

launch::async means they start immediately. launch::deferred means they only start when you ask for the result. You do not worry about managing them - the system handles everything and cleans up automatically.

Best Practices

Specify launch policy: Always use std::launch::async or std::launch::deferred explicitly instead of relying on the default.

Store futures: Always store the future returned by std::async. If you discard it, the destructor blocks waiting for completion.

Use for task parallelism: std::async is perfect for independent tasks that return results, not for complex threading patterns.

Handle exceptions on get(): Always wrap future.get() in try-catch to handle exceptions from async tasks.

Avoid over-subscribing: Launching too many async tasks can create more threads than CPU cores, reducing performance.

Common Mistakes

Discarding the future: If you do not store the future, the destructor blocks immediately, defeating async purpose.

Not specifying launch policy: Default policy lets implementation choose, which may not be what you want.

Creating too many async tasks: Each async task may create a new thread. Use thread pools for many tasks.

Expecting immediate execution: Without std::launch::async, the task might not run until get() is called.

Debug Challenge

This async call is missing the launch policy for guaranteed parallel execution. Click the highlighted line to fix it:

1 int expensiveComputation() {
2 // Do expensive work
3 return 42;
4 }
5
6 int main() {
7 auto future{std::async(expensiveComputation)};
8 int result{future.get()};
9 }

Quick Quiz

  1. What is the difference between std::launch::async and std::launch::deferred?
async runs immediately on a new thread, deferred waits until get() is called
async is faster than deferred
deferred uses more memory than async
  1. How does std::async handle exceptions?
Exceptions crash the program
Exceptions are stored in the future and re-thrown when get() is called
Exceptions are printed to stderr
  1. When should you use std::async instead of std::thread?
For long-running background threads
For fine-grained thread control
For task-based parallelism where you need the return value

Practice Playground

Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.

Lesson Progress

  • Fix This Code
  • Quick Quiz
  • Practice Playground - run once