Skip to Content
Course content

190: Async Programming with std::async

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this exact bug trip up dozens of developers who are moving from synchronous code to concurrency. They discover std::async, see that it returns a std::future, and assume that because they don't actually need the return value of the function they're calling, they can just ignore it. It looks clean, but it creates a performance nightmare.

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

void load_heavy_resource(std::string name) {
    std::cout << "Loading " << name << "...\n";
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << name << " loaded!\n";
}

void initialize_app() {
    // I want these to run in parallel to save time
    std::async(std::launch::async, load_heavy_resource, "Database");
    std::async(std::launch::async, load_heavy_resource, "Config File");
    std::async(std::launch::async, load_heavy_resource, "Network Cache");
    
    std::cout << "App initialized!\n";
}

int main() {
    initialize_app();
    return 0;
}

The Hidden Synchronous Trap

If you run this, you'll expect it to take about 2 seconds because the three loading tasks should happen simultaneously. Instead, it takes 6 seconds. The "Loading" messages appear one by one, and the program hangs on each line. You've written asynchronous code that is executing synchronously.

Here is why: std::async returns a std::future object. In C++, if you don't capture that return value, it becomes a temporary object. The destructor of a std::future returned by std::async has a very specific, and somewhat controversial, behavior: it blocks until the asynchronous task completes.

So, when you call std::async(...) without assigning it to a variable, the temporary future is destroyed at the end of that same statement. Your main thread stops and waits for "Database" to finish before it even moves to the next line to start "Config File". You've effectively created a very expensive way to call a function normally.

Capturing the Future to Enable Parallelism

To actually get these tasks to run in the background, you have to keep the std::future objects alive. By storing them in a container or separate variables, you prevent the destructor from running immediately, allowing the threads to actually work in parallel.

void initialize_app() {
    // We store the futures in a vector to keep them alive
    std::vector<std::future<void>> futures;

    futures.push_back(std::async(std::launch::async, load_heavy_resource, "Database"));
    futures.push_back(std::async(std::launch::async, load_heavy_resource, "Config File"));
    futures.push_back(std::async(std::launch::async, load_heavy_resource, "Network Cache"));
    
    std::cout << "Tasks started, now doing other work...\n";

    // Now we explicitly wait for them to finish
    for(auto &f : futures) {
        f.get(); 
    }
    
    std::cout << "App initialized!\n";
}

Now the behavior changes. The loop at the end ensures that we don't exit the function before the work is done, but the std::async calls themselves return almost instantly. The total execution time drops from 6 seconds to roughly 2 seconds.

Choosing Your Execution Policy

You'll notice I used std::launch::async. This is important. If you omit the policy or use std::launch::deferred, the behavior changes entirely. std::launch::deferred tells C++: "Don't start this now; just run it when I eventually call .get() or .wait() on the future."

I usually recommend being explicit. If you want a new thread, use std::launch::async. If you want the system to decide based on current CPU load (which is the default behavior of just calling std::async(fn)), be aware that it might choose to defer the task, leaving you wondering why your "background" task hasn't started yet.




📋 Practical Task

Build a Parallel Prime Number Range Summation Tool

Your goal is to write a program that calculates the sum of all prime numbers in a large range (e.g., 1 to 1,000,000). To make it efficient, you must split the range into four equal segments and process each segment asynchronously using std::async.

  • Create a helper function long long sum_primes(int start, int end) that iterates through the range and sums the primes.
  • In main, launch four asynchronous tasks using std::launch::async, each covering one quarter of the range.
  • Store the resulting std::future<long long> objects in a container.
  • Aggregate the final sum by calling .get() on each future.
  • Measure and print the total time taken to ensure the work is actually happening in parallel.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.