好的,让我们开始深入探讨 C++ std::future 和 std::promise 的同步机制,以及它们如何实现异步结果的等待与获取。
讲座:C++ std::future/std::promise 同步机制详解
大家好,今天我们要深入探讨 C++ 标准库中用于异步编程的关键工具:std::future 和 std::promise。理解它们的工作原理对于编写高效、并发和响应式的 C++ 应用程序至关重要。
1. 异步编程的必要性
在现代软件开发中,异步编程变得越来越重要。原因有很多:
- 提高响应性: 避免阻塞主线程,保持用户界面的流畅和响应。
- 并发执行: 利用多核 CPU 的优势,并行执行任务,缩短整体执行时间。
- 资源利用率: 允许一个线程在等待 I/O 操作完成时执行其他任务,提高资源利用率。
2. std::future 和 std::promise 的角色
std::future 和 std::promise 是一对协同工作的类,它们充当异步操作结果的“占位符”和“生产者”。
std::promise: 负责设置(或“承诺”)异步操作的结果。它是结果的“生产者”。std::future: 负责获取异步操作的结果。它是结果的“消费者”。std::future对象通常与一个std::promise对象关联,并可以等待(阻塞)直到std::promise设置了结果。
3. 基本用法示例
让我们通过一个简单的例子来理解它们的基本用法。
#include <iostream>
#include <future>
#include <thread>
int calculate_sum(int a, int b) {
std::cout << "Calculating sum in a separate thread..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
return a + b;
}
int main() {
// 1. 创建一个 std::promise 对象
std::promise<int> promise;
// 2. 从 promise 中获取一个 std::future 对象
std::future<int> future = promise.get_future();
// 3. 创建一个线程,在该线程中执行异步操作
std::thread worker_thread([&promise]() {
try {
// 执行计算,并将结果设置到 promise 中
int result = calculate_sum(5, 3);
promise.set_value(result);
} catch (...) {
// 如果发生异常,将异常设置到 promise 中
promise.set_exception(std::current_exception());
}
});
// 4. 在主线程中,等待 future 对象的结果
std::cout << "Waiting for the result..." << std::endl;
try {
int sum = future.get(); // 阻塞,直到结果可用
std::cout << "Sum: " << sum << std::endl;
} catch (const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
}
// 5. 等待工作线程完成
worker_thread.join();
return 0;
}
代码解释:
std::promise<int> promise;: 创建了一个std::promise对象,它承诺提供一个int类型的结果。std::future<int> future = promise.get_future();: 从promise对象中获取一个std::future对象。这个future对象将用于获取结果。std::thread worker_thread(...): 创建了一个新的线程,用于执行calculate_sum函数(模拟异步操作)。promise.set_value(result);: 在工作线程中,计算完成后,使用promise.set_value()将结果设置到promise对象中。 这会解除future.get()的阻塞。promise.set_exception(std::current_exception());: 如果异步操作抛出异常,使用promise.set_exception()将异常设置到promise对象中。future.get()会抛出相同的异常。int sum = future.get();: 在主线程中,调用future.get()来等待结果。future.get()会阻塞,直到promise对象设置了值或异常。worker_thread.join();: 等待工作线程完成,防止主线程提前退出。
4. 异常处理
重要的是要正确处理异步操作中可能发生的异常。如上面的代码所示,我们使用了 try...catch 块来捕获异常,并使用 promise.set_exception() 将异常传递给 future 对象。 future.get() 会重新抛出该异常,允许调用者处理它。
5. std::future 的不同获取方式
std::future 提供了几种获取结果的方式:
get(): 阻塞,直到结果可用,然后返回结果。 如果promise设置了异常,则会重新抛出该异常。get()只能调用一次,多次调用会导致程序崩溃(移动语义)。wait(): 阻塞,直到结果可用或超时。不返回任何值。wait_for(): 阻塞,直到结果可用或达到指定的超时时间。不返回任何值。 返回一个std::future_status枚举值,指示等待状态:std::future_status::ready: 结果已准备好。std::future_status::timeout: 等待超时。std::future_status::deferred: future 关联的任务尚未启动(仅适用于std::async,稍后讨论)。
wait_until(): 阻塞,直到结果可用或达到指定的截止时间。不返回任何值。返回一个std::future_status枚举值。valid(): 检查future对象是否有效(即,是否与某个共享状态关联)。
示例:使用 wait_for() 进行超时处理
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
int main() {
std::promise<int> promise;
std::future<int> future = promise.get_future();
std::thread worker_thread([&promise]() {
std::this_thread::sleep_for(std::chrono::seconds(5)); // 模拟耗时操作
promise.set_value(42);
});
std::cout << "Waiting for the result with a timeout..." << std::endl;
auto status = future.wait_for(std::chrono::seconds(3));
if (status == std::future_status::ready) {
std::cout << "Result is ready: " << future.get() << std::endl;
} else if (status == std::future_status::timeout) {
std::cout << "Timeout occurred. Result is not ready yet." << std::endl;
} else if (status == std::future_status::deferred) {
std::cout << "Deferred." << std::endl; // 这不会发生,因为我们手动创建了线程
}
worker_thread.join();
return 0;
}
在这个例子中,wait_for() 设置了 3 秒的超时时间。如果 3 秒后结果仍然不可用,它会返回 std::future_status::timeout。
6. std::shared_future
std::future 对象只能调用一次 get()。 如果需要多个线程访问相同的结果,可以使用 std::shared_future。 std::shared_future 允许多个线程安全地访问相同的结果。
#include <iostream>
#include <future>
#include <thread>
#include <vector>
int main() {
std::promise<int> promise;
std::future<int> future = promise.get_future();
std::shared_future<int> shared_future = future.share(); // 将 future 转换为 shared_future
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
threads.emplace_back([&shared_future, i]() {
std::cout << "Thread " << i << " waiting for the result..." << std::endl;
int result = shared_future.get(); // 多个线程可以同时调用 get()
std::cout << "Thread " << i << " received result: " << result << std::endl;
});
}
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟一些延迟
promise.set_value(42);
for (auto& thread : threads) {
thread.join();
}
return 0;
}
7. std::async
std::async 是一个方便的函数,用于启动异步任务并返回一个 std::future 对象。 它简化了手动创建线程和 std::promise 的过程。
#include <iostream>
#include <future>
int calculate_product(int a, int b) {
std::cout << "Calculating product in a separate thread..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
return a * b;
}
int main() {
// 使用 std::async 启动异步任务
std::future<int> future = std::async(std::launch::async, calculate_product, 7, 6);
std::cout << "Waiting for the result from std::async..." << std::endl;
int product = future.get();
std::cout << "Product: " << product << std::endl;
return 0;
}
std::async 的启动策略:
std::launch::async: 强制在新的线程中异步执行任务。std::launch::deferred: 延迟执行任务,直到调用future.get()或future.wait()。 任务将在调用get()或wait()的线程中同步执行。std::launch::any: 由系统决定是异步执行还是延迟执行。 这是默认策略。
示例:std::launch::deferred
#include <iostream>
#include <future>
int calculate_square(int x) {
std::cout << "Calculating square in thread: " << std::this_thread::get_id() << std::endl;
return x * x;
}
int main() {
std::cout << "Main thread: " << std::this_thread::get_id() << std::endl;
std::future<int> future = std::async(std::launch::deferred, calculate_square, 5);
std::cout << "Before future.get()" << std::endl;
int square = future.get(); // 任务将在主线程中执行
std::cout << "Square: " << square << std::endl;
std::cout << "After future.get()" << std::endl;
return 0;
}
在这个例子中,由于使用了 std::launch::deferred,calculate_square 函数将在主线程中执行,而不是在单独的线程中执行。 只有在调用 future.get() 时才会执行。
8. std::packaged_task
std::packaged_task 是一种将函数或可调用对象包装成异步任务的类。 它与 std::future 和 std::promise 结合使用,允许将任意函数转换为异步操作。
#include <iostream>
#include <future>
#include <thread>
int calculate_factorial(int n) {
if (n <= 1) {
return 1;
}
return n * calculate_factorial(n - 1);
}
int main() {
// 1. 创建一个 std::packaged_task 对象
std::packaged_task<int(int)> task(calculate_factorial);
// 2. 从 task 中获取一个 std::future 对象
std::future<int> future = task.get_future();
// 3. 创建一个线程,在该线程中执行任务
std::thread worker_thread([&task]() {
task(5); // 调用 task,执行 calculate_factorial(5)
});
// 4. 在主线程中,等待 future 对象的结果
std::cout << "Waiting for the factorial result..." << std::endl;
int factorial = future.get();
std::cout << "Factorial: " << factorial << std::endl;
worker_thread.join();
return 0;
}
9. 总结对比:std::promise, std::async, std::packaged_task
| 特性 | std::promise |
std::async |
std::packaged_task |
|---|---|---|---|
| 主要用途 | 手动设置异步操作的结果或异常。 | 启动异步任务并返回 std::future。 |
将函数或可调用对象包装成异步任务。 |
| 线程管理 | 需要手动创建和管理线程。 | 自动管理线程(可以指定启动策略)。 | 需要手动创建和管理线程。 |
| 异常处理 | 需要手动设置异常。 | 自动处理异常,并将异常传递给 std::future。 |
需要手动设置异常,或者让异常自动传播(取决于函数是否抛出异常)。 |
| 灵活性 | 最高的灵活性,可以完全控制异步操作的流程。 | 较高的灵活性,可以指定启动策略。 | 较高的灵活性,可以将任意函数或可调用对象转换为异步任务。 |
| 易用性 | 相对较低,需要手动管理线程和异常。 | 相对较高,简化了线程管理和异常处理。 | 相对较低,需要手动管理线程,但简化了将函数转换为异步任务的过程。 |
| 使用场景 | 需要完全控制异步操作流程,例如自定义线程池。 | 简单的异步任务,不需要精细的线程管理。 | 需要将现有函数转换为异步任务,并进行更复杂的控制。 |
10. 总结:掌握异步编程的关键
std::future 和 std::promise 提供了一种强大的机制,用于在 C++ 中进行异步编程。 它们允许我们将耗时的操作转移到后台线程,从而保持主线程的响应性。 通过结合 std::async 和 std::packaged_task,我们可以更方便地创建和管理异步任务。 理解和掌握这些工具对于编写高效、并发的 C++ 应用程序至关重要。
更多IT精英技术系列讲座,到智猿学院