C++ std::async: Multi-Thread Async Execution Guide and Examples
C++ std::async: Multi-Thread Async Execution Guide and Examples
std::async is a high-level C++ standard library function that launches asynchronous tasks and returns a std::future to access the result. It provides a simple way to execute functions asynchronously without manually managing threads, making it ideal for fire-and-forget tasks and parallel computations.
Table of Contents
- What is std::async?
- Basic Usage and API
- Launch Policies
- Example 1: Simple Async Task
- Example 2: Parallel Computation
- Example 3: Multiple Async Tasks
- Example 4: Async with Error Handling
- Example 5: Deferred Execution
- Example 6: Async with Shared State
- Best Practices
- Common Pitfalls
What is std::async?
std::async is a function template that:
- Launches a function asynchronously (or synchronously)
- Returns a
std::futureto access the result - Manages thread lifecycle automatically
- Supports launch policies to control execution behavior
Key Benefits
- Simple API: No need to manually create threads
- Automatic resource management: Threads are managed by the standard library
- Future-based: Get results when ready
- Exception safety: Exceptions are propagated through futures
- Flexible execution: Can run immediately or deferred
When to Use std::async
// GOOD: Use std::async for simple async tasks
auto future = async(launch::async, []() {
return computeExpensiveResult();
});
auto result = future.get();
// BAD: Manual thread management for simple tasks
promise<int> p;
auto f = p.get_future();
thread t([&p]() {
p.set_value(computeExpensiveResult());
});
t.join();
auto result = f.get(); // More verbose!
Basic Usage and API
Basic Syntax
#include <future>
#include <iostream>
using namespace std;
// Basic async call
auto future = async(launch::async, function, args...);
auto result = future.get(); // Wait and get result
Function Signature
template<class Function, class... Args>
future<typename result_of<Function(Args...)>::type>
async(launch policy, Function&& f, Args&&... args);
Key Components
std::async: Launches the async taskstd::future<T>: Handle to access the result- Launch Policy: Controls when/how the task executes
get(): Waits for result and retrieves itwait(): Waits without retrieving result
Launch Policies
std::launch::async
Executes the function asynchronously in a new thread:
auto future = async(launch::async, []() {
return 42;
});
// Function starts executing immediately
auto result = future.get(); // Waits if not done
std::launch::deferred
Defers execution until get() or wait() is called:
auto future = async(launch::deferred, []() {
return 42;
});
// Function hasn't started yet
auto result = future.get(); // Executes now, synchronously
std::launch::async | std::launch::deferred (Default)
Implementation chooses the policy (usually async):
auto future = async([]() { // Default policy
return 42;
});
Example 1: Simple Async Task
Basic async execution with result retrieval:
#include <future>
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
int computeValue(int x) {
this_thread::sleep_for(chrono::seconds(1));
return x * 2;
}
int main() {
cout << "Starting async task..." << endl;
// Launch async task
auto future = async(launch::async, computeValue, 21);
// Do other work while task runs
cout << "Doing other work..." << endl;
this_thread::sleep_for(chrono::milliseconds(500));
// Get result (waits if not ready)
int result = future.get();
cout << "Result: " << result << endl; // Output: Result: 42
return 0;
}
Output:
Starting async task...
Doing other work...
Result: 42
Example 2: Parallel Computation
Parallel processing of multiple independent computations:
#include <future>
#include <vector>
#include <iostream>
#include <numeric>
#include <algorithm>
using namespace std;
int processChunk(const vector<int>& data, size_t start, size_t end) {
int sum = 0;
for (size_t i = start; i < end; ++i) {
sum += data[i] * data[i];
}
return sum;
}
int main() {
vector<int> data(1000000);
iota(data.begin(), data.end(), 1); // Fill with 1..1000000
const size_t num_threads = 4;
const size_t chunk_size = data.size() / num_threads;
vector<future<int>> futures;
// Launch parallel tasks
for (size_t i = 0; i < num_threads; ++i) {
size_t start = i * chunk_size;
size_t end = (i == num_threads - 1) ? data.size() : (i + 1) * chunk_size;
futures.push_back(async(launch::async, processChunk,
cref(data), start, end));
}
// Collect results
int total = 0;
for (auto& f : futures) {
total += f.get();
}
cout << "Total sum: " << total << endl;
return 0;
}
Example 3: Multiple Async Tasks
Managing multiple independent async tasks:
#include <future>
#include <vector>
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
using namespace std;
string fetchData(const string& url) {
this_thread::sleep_for(chrono::milliseconds(100));
return "Data from " + url;
}
int main() {
vector<string> urls = {
"http://api1.com",
"http://api2.com",
"http://api3.com",
"http://api4.com"
};
vector<future<string>> futures;
// Launch all async tasks
for (const auto& url : urls) {
futures.push_back(async(launch::async, fetchData, url));
}
// Process results as they complete
for (size_t i = 0; i < futures.size(); ++i) {
string result = futures[i].get();
cout << "Received: " << result << endl;
}
return 0;
}
Output:
Received: Data from http://api1.com
Received: Data from http://api2.com
Received: Data from http://api3.com
Received: Data from http://api4.com
Example 4: Async with Error Handling
Handling exceptions in async tasks:
#include <future>
#include <iostream>
#include <stdexcept>
using namespace std;
int riskyComputation(int x) {
if (x < 0) {
throw invalid_argument("Negative value not allowed");
}
return x * 2;
}
int main() {
try {
// Launch async task that might throw
auto future = async(launch::async, riskyComputation, -5);
// Exception is propagated when get() is called
int result = future.get();
cout << "Result: " << result << endl;
}
catch (const exception& e) {
cout << "Error: " << e.what() << endl;
}
// Successful case
try {
auto future2 = async(launch::async, riskyComputation, 10);
int result = future2.get();
cout << "Result: " << result << endl; // Output: Result: 20
}
catch (const exception& e) {
cout << "Error: " << e.what() << endl;
}
return 0;
}
Output:
Error: Negative value not allowed
Result: 20
Example 5: Deferred Execution
Using deferred launch policy for lazy evaluation:
#include <future>
#include <iostream>
#include <chrono>
using namespace std;
using namespace chrono;
int expensiveComputation() {
cout << "Computing..." << endl;
this_thread::sleep_for(seconds(2));
return 42;
}
int main() {
// Deferred: doesn't start until get() is called
auto future = async(launch::deferred, expensiveComputation);
cout << "Task created, but not started yet" << endl;
this_thread::sleep_for(seconds(1));
cout << "Now calling get()..." << endl;
auto start = high_resolution_clock::now();
int result = future.get(); // Executes synchronously here
auto end = high_resolution_clock::now();
auto duration = duration_cast<milliseconds>(end - start);
cout << "Result: " << result << endl;
cout << "Took: " << duration.count() << " ms" << endl;
return 0;
}
Output:
Task created, but not started yet
Now calling get()...
Computing...
Result: 42
Took: 2000 ms
Example 6: Async with Shared State
Sharing state between async tasks and main thread:
#include <future>
#include <iostream>
#include <vector>
#include <mutex>
#include <atomic>
using namespace std;
class SharedCounter {
private:
atomic<int> count_{0};
mutex mtx_;
public:
void increment() {
lock_guard<mutex> lock(mtx_);
count_++;
}
int get() const {
return count_.load();
}
};
void worker(SharedCounter& counter, int iterations) {
for (int i = 0; i < iterations; ++i) {
counter.increment();
}
}
int main() {
SharedCounter counter;
const int num_workers = 4;
const int iterations_per_worker = 1000;
vector<future<void>> futures;
// Launch workers
for (int i = 0; i < num_workers; ++i) {
futures.push_back(async(launch::async, worker,
ref(counter), iterations_per_worker));
}
// Wait for all workers
for (auto& f : futures) {
f.wait();
}
cout << "Final count: " << counter.get() << endl;
cout << "Expected: " << num_workers * iterations_per_worker << endl;
return 0;
}
Output:
Final count: 4000
Expected: 4000
Best Practices
1. Always Store the Future
// GOOD: Store future to prevent immediate destruction
auto future = async(launch::async, task);
// ... do other work ...
auto result = future.get();
// BAD: Future destroyed immediately, task may be cancelled
async(launch::async, task); // Future destroyed!
2. Use Appropriate Launch Policy
// For CPU-bound tasks
auto future = async(launch::async, cpuBoundTask);
// For lazy evaluation
auto future = async(launch::deferred, lazyTask);
// Let implementation decide (default)
auto future = async(task);
3. Handle Exceptions
try {
auto future = async(launch::async, riskyTask);
auto result = future.get();
}
catch (const exception& e) {
// Handle exception
}
4. Use wait_for() for Timeouts
auto future = async(launch::async, longRunningTask);
if (future.wait_for(chrono::seconds(5)) == future_status::ready) {
auto result = future.get();
} else {
// Task still running
}
5. Avoid Too Many Concurrent Tasks
// GOOD: Limit concurrent tasks
const int max_concurrent = 4;
vector<future<int>> futures;
for (int i = 0; i < 100; ++i) {
if (futures.size() >= max_concurrent) {
futures[0].wait(); // Wait for one to complete
futures.erase(futures.begin());
}
futures.push_back(async(launch::async, task, i));
}
// BAD: Creating too many tasks
for (int i = 0; i < 10000; ++i) {
async(launch::async, task, i); // May exhaust resources
}
Common Pitfalls
1. Forgetting to Store the Future
// BAD: Future destroyed immediately
async(launch::async, []() {
// This may never execute!
});
// GOOD: Store future
auto future = async(launch::async, []() {
// This will execute
});
future.wait();
2. Calling get() Multiple Times
auto future = async(launch::async, []() { return 42; });
int result1 = future.get(); // OK
int result2 = future.get(); // ERROR: future is invalid!
3. Not Handling Exceptions
// BAD: Exception may terminate program
auto future = async(launch::async, []() {
throw runtime_error("Error!");
});
int result = future.get(); // Throws unhandled exception
// GOOD: Handle exceptions
try {
auto future = async(launch::async, []() {
throw runtime_error("Error!");
});
int result = future.get();
}
catch (const exception& e) {
// Handle error
}
4. Race Conditions with Shared Data
// BAD: Unsynchronized access
int counter = 0;
auto future = async(launch::async, [&counter]() {
counter++; // Race condition!
});
// GOOD: Use synchronization
mutex mtx;
int counter = 0;
auto future = async(launch::async, [&counter, &mtx]() {
lock_guard<mutex> lock(mtx);
counter++; // Safe
});
5. Blocking on get() in Critical Paths
// BAD: Blocking main thread
auto future = async(launch::async, longTask);
int result = future.get(); // Blocks here
// Can't do anything else
// GOOD: Check if ready first
auto future = async(launch::async, longTask);
if (future.wait_for(chrono::milliseconds(100)) == future_status::ready) {
int result = future.get();
} else {
// Do other work, check later
}
6. Using Deferred When Async is Needed
// BAD: Deferred executes synchronously
auto future = async(launch::deferred, task);
// ... do other work ...
int result = future.get(); // Executes synchronously here!
// GOOD: Use async for true parallelism
auto future = async(launch::async, task);
// ... do other work ...
int result = future.get(); // Already running in parallel
Summary
std::async provides a simple and powerful way to execute tasks asynchronously in C++:
- Simple API: No manual thread management needed
- Future-based: Get results when ready
- Exception safe: Exceptions propagate through futures
- Flexible: Support for async and deferred execution
- Standard library: Part of C++11 standard
Key Takeaways
- Always store the
futurereturned byasync - Use appropriate launch policies for your use case
- Handle exceptions when calling
get() - Avoid calling
get()multiple times - Use synchronization for shared data access
- Consider using
wait_for()for non-blocking checks
When to Use std::async
- Simple async tasks that return a value
- Parallel computations that can be split into independent tasks
- Fire-and-forget tasks with result retrieval
- When you need exception propagation from async tasks
When NOT to Use std::async
- Complex thread pool requirements (use custom thread pool)
- Tasks that need fine-grained control over thread lifecycle
- When you need to cancel tasks (use
std::threadwith cancellation tokens) - Very high-frequency task spawning (overhead may be too high)
By understanding std::async and following best practices, you can write efficient and maintainable concurrent C++ code.