In the previous chapters, we learned how to explicitly create and manage threads using <span>std::thread</span>. This is a powerful and straightforward method, but it can sometimes feel cumbersome. Often, we just want to execute a task asynchronously and retrieve its result at some point in the future without manually handling the tedious details of thread creation, joining, and data synchronization.
The <span><future></span> header file introduced in C++11 provides us with a higher-level abstraction for asynchronous programming. The core idea is to allow a task (function) to run automatically in the background while we hold a “promise” (future) that we can use to retrieve the result of the task later. The two most important components in this are <span>std::future</span> and <span>std::async</span>.
1. Core Concept: What is std::future?
We can think of <span>std::future</span> as a channel or handle to a future result. You start an asynchronous task, and this task will eventually produce a result (which could be a value or an exception).<span>std::future</span> objects are your only proof of obtaining this result.
It mainly provides the following methods:
<span>get()</span>: Retrieve the result. This is a blocking call. If the asynchronous task has not yet completed, the thread calling<span>get()</span>will be blocked until the task is finished, then it returns the result (or throws an exception). Note:<span>get()</span>can only be called once; after calling, the future becomes invalid.<span>wait()</span>: Wait for completion. Blocks the current thread until the asynchronous task is complete, but does not retrieve the result.<span>wait_for()</span>/<span>wait_until()</span>: Wait with a timeout. Waits for the task to complete within a specified time frame and returns its status (ready, timeout, or delayed).
In simple terms, <span>std::future</span> is the end that “fetches the result”.
2. Starting Asynchronous Tasks: std::async
<span>std::async</span> is a function template responsible for starting an asynchronous task and immediately returning a <span>std::future</span> object as a “promise” for that task.
Its basic usage is as follows:
#include <iostream>
#include <future>
#include <chrono>
// A time-consuming computation function
int expensive_computation(int x, int y) {
// Simulate a time-consuming operation
std::this_thread::sleep_for(std::chrono::seconds(2));
return x + y;
}
int main() {
// Use std::async to start an asynchronous task
// It will automatically execute expensive_computation(10, 20) in a background thread
std::future<int> result_future = std::async(expensive_computation, 10, 20);
// Meanwhile, do some other work in the main thread...
std::cout << "Main thread is doing other work..." << std::endl;
// ... (this work will run concurrently with the asynchronous task)
// Now we need the result of the asynchronous task, call get() to retrieve it
// If the task is not yet complete, the main thread will block here waiting
int result = result_future.get();
std::cout << "The result is: " << result << std::endl; // Outputs 30
return 0;
}
Launch Policy
<span>std::async</span> behavior can be controlled by an additional parameter that determines whether the task is executed immediately in a new thread or delayed until calling <span>get()</span> / <span>wait()</span>.
<span>std::launch::async</span>: Asynchronous execution. The function will definitely start executing immediately in a new thread.<span>std::launch::deferred</span>: Deferred execution. The function will not execute immediately but will be delayed until the first call to<span>get()</span>or<span>wait()</span>, executing synchronously in the requesting thread. If neither<span>get()</span>nor<span>wait()</span>is called, the function will not execute at all.
You can explicitly specify the policy or leave it unspecified (default behavior), allowing the compiler implementation to decide how to schedule, which is usually either option.
// Explicitly specify asynchronous execution (guaranteed to run in a new thread)
auto f1 = std::async(std::launch::async, expensive_computation, 10, 20);
// Explicitly specify deferred execution (delayed until .get() is called in the current thread)
auto f2 = std::async(std::launch::deferred, expensive_computation, 10, 20);
// ... At this point, expensive_computation has not yet executed
int r = f2.get(); // At this moment, synchronously call expensive_computation in the current thread
// Default policy (decided by implementation, could be async | deferred)
auto f3 = std::async(expensive_computation, 10, 20);
3. A Complete Example: Experience Concurrency Acceleration
The following example demonstrates how to use <span>std::async</span> to concurrently execute multiple tasks, thereby accelerating the program.
#include <iostream>
#include <vector>
#include <future>
#include <chrono>
// Simulate a time-consuming task, calculating the sum of integers from start to end
long long sequential_sum(long long start, long long end) {
long long sum = 0;
for (long long i = start; i <= end; ++i) {
sum += i;
}
return sum;
}
int main() {
const long long number = 1000000000; // 1 billion
// Method 1: Serial computation
auto start_time = std::chrono::high_resolution_clock::now();
long long sum1 = sequential_sum(1, number);
auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
std::cout << "Sequential result: " << sum1 << ", Time: " << duration.count() << " ms" << std::endl;
// Method 2: Asynchronous parallel computation (divided into 4 segments)
start_time = std::chrono::high_resolution_clock::now();
// Create 4 asynchronous tasks
std::future<long long> f1 = std::async(std::launch::async, sequential_sum, 1, number / 4);
std::future<long long> f2 = std::async(std::launch::async, sequential_sum, number / 4 + 1, number / 2);
std::future<long long> f3 = std::async(std::launch::async, sequential_sum, number / 2 + 1, number * 3 / 4);
std::future<long long> f4 = std::async(std::launch::async, sequential_sum, number * 3 / 4 + 1, number);
// Retrieve results from each part and sum them up
long long sum2 = f1.get() + f2.get() + f3.get() + f4.get();
end_time = std::chrono::high_resolution_clock::now();
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
std::cout << "Parallel result: " << sum2 << ", Time: " << duration.count() << " ms" << std::endl;
return 0;
}
- The output might look like:
Sequential result: 500000000500000000, Time: 2894 ms
Parallel result: 500000000500000000, Time: 742 ms
This example clearly demonstrates the performance improvement gained by breaking a large task into smaller tasks and executing them concurrently using <span>std::async</span>.
4. Exception Handling
<span>std::future</span> has another significant advantage: it can automatically propagate exceptions. If an exception is thrown in the asynchronous task, it will not terminate the program in the background thread; instead, the exception will be captured and stored in the <span>std::future</span>. When you call <span>get()</span> in the main thread, this stored exception will be re-thrown in the current thread.
#include <future>
#include <iostream>
void might_throw() {
throw std::runtime_error("Something went wrong in the async task!");
}
int main() {
auto f = std::async(std::launch::async, might_throw);
try {
f.get(); // Here, the exception thrown in the async task will be caught by the main thread
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
return 0;
}
5. Summary and Best Practices
- Higher-level abstraction:
<span>std::async</span>+<span>std::future</span>is simpler than directly using<span>std::thread</span>, as it automatically handles thread creation, result passing, and exception propagation. - Default policy pitfalls: The default launch policy (
<span>std::launch::async | std::launch::deferred</span>) has uncertainty. If you explicitly need concurrency, always use the<span>std::launch::async</span>policy; otherwise, tasks may be delayed, preventing true asynchrony. <span>get()</span>is one-time:<span>std::future::get()</span>is a move operation; after calling, the future state becomes invalid. A future can only retrieve a result once.- Applicable scenarios: Ideal for “fire-and-forget” or independent computation tasks that require results. For tasks needing complex synchronization or frequent communication, you may still need to use
<span>std::thread</span>and lower-level primitives like mutexes.
In summary, <span>std::async</span> and <span>std::future</span> are powerful tools for asynchronous programming in C++, making it easier and safer to write concurrent code, and they are essential tools that every C++ developer should master.