C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
190: Async Programming with std::async
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 usingstd::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.
There are no comments for now.