好的,各位朋友,欢迎来到今天的C++异步编程小课堂!今天咱们要聊的是C++标准库里一对好基友:std::promise
和 std::future
。 这俩哥们儿,一个负责承诺,一个负责等待,完美诠释了什么叫“信任”。
一、背景故事:为什么需要 promise
和 future
?
想象一下,你是一家餐厅的老板。客人点了菜,厨房开始做菜。客人(主线程)不能傻等着菜做好,得继续招呼其他客人。厨房(异步任务)做好菜后,需要通知客人可以上菜了。
在多线程编程中,我们也经常遇到类似的情况。主线程启动一个异步任务,然后继续做其他事情。异步任务完成计算后,需要把结果传递给主线程。并且,主线程需要在某个时刻等待异步任务的结果。
直接共享变量加锁是个办法,但容易出错,而且代码丑陋。std::promise
和 std::future
就是为了优雅地解决这个问题而生的。它们提供了一种线程安全的、基于承诺的异步结果传递和等待机制。
二、std::promise
:信守承诺的家伙
std::promise
就像厨房里的厨师,负责承诺给客人一道美味佳肴(一个值)。它提供了一种设置异步任务结果的方式。
-
核心功能: 设置值(
set_value
)或设置异常(set_exception
)。 -
使用场景: 通常在异步任务中创建,用于存储任务的结果或异常。
-
代码示例:
#include <iostream>
#include <future>
#include <thread>
#include <exception>
int calculate_sum(int a, int b, std::promise<int> prom) {
try {
if (a < 0 || b < 0) {
throw std::runtime_error("Negative numbers are not allowed!");
}
int sum = a + b;
prom.set_value(sum); // 承诺:我算好啦!
std::cout << "计算线程: 计算结果 " << sum << " 并设置 promise" << std::endl;
return sum;
} catch (const std::exception& e) {
prom.set_exception(std::current_exception()); // 承诺:我出错了!
std::cerr << "计算线程: 发生异常: " << e.what() << std::endl;
return -1; // 或者其他表示错误的返回值
}
}
int main() {
std::promise<int> promise; // 创建一个 promise
std::future<int> future = promise.get_future(); // 获取与 promise 关联的 future
std::thread t(calculate_sum, 5, 3, std::move(promise)); // 启动异步任务,传递 promise
try {
std::cout << "主线程: 等待计算结果..." << std::endl;
int result = future.get(); // 等待结果,阻塞直到 promise 被设置
std::cout << "主线程: 收到计算结果: " << result << std::endl;
} catch (const std::exception& e) {
std::cerr << "主线程: 捕获到异常: " << e.what() << std::endl;
}
t.join(); // 等待线程结束
return 0;
}
代码解释:
-
std::promise<int> promise;
:创建了一个promise
对象,它承诺最终会提供一个int
类型的值。 -
std::future<int> future = promise.get_future();
:从promise
对象获取一个future
对象。future
对象是用来获取promise
所承诺的值的。 -
std::thread t(calculate_sum, 5, 3, std::move(promise));
:创建一个新线程,执行calculate_sum
函数。注意,promise
对象通过std::move
传递给线程。这是因为promise
对象只能与一个future
对象关联,并且只能被设置一次。 -
prom.set_value(sum);
:在calculate_sum
函数中,计算完成后,使用set_value
方法设置promise
的值。 -
future.get();
:在主线程中,使用future.get()
方法等待promise
的值。这个方法会阻塞,直到promise
的值被设置。如果promise
设置的是异常,future.get()
会抛出相同的异常。
重点:
promise
只能设置一次值或异常。重复设置会导致异常。promise
对象必须在线程之间传递,通常使用std::move
。promise
的作用域结束时,如果还没有设置值或异常,会抛出std::future_error
异常。
三、std::future
:耐心等待的家伙
std::future
就像等待上菜的客人,它提供了一种获取异步任务结果的方式。
-
核心功能: 获取值(
get
)、检查状态(valid
、wait
、wait_for
、wait_until
)。 -
使用场景: 通常在主线程中创建,用于等待并获取异步任务的结果。
-
代码示例(延续上面的例子):
(上面的例子已经包含了 future
的基本使用)
future
的更多用法:
valid()
: 检查future
对象是否有效,即是否与一个promise
对象关联。
if (future.valid()) {
std::cout << "Future 对象有效" << std::endl;
} else {
std::cout << "Future 对象无效" << std::endl;
}
wait()
: 阻塞当前线程,直到future
对象准备好(即promise
被设置了值或异常)。
std::cout << "主线程: 等待 future 准备好..." << std::endl;
future.wait(); // 阻塞直到 future 准备好
std::cout << "主线程: future 准备好了!" << std::endl;
wait_for()
: 阻塞当前线程一段时间,如果在指定时间内future
对象没有准备好,则返回。
std::cout << "主线程: 等待 future 准备好 (最多 2 秒)..." << std::endl;
auto status = future.wait_for(std::chrono::seconds(2));
if (status == std::future_status::ready) {
std::cout << "主线程: future 准备好了!" << std::endl;
} else if (status == std::future_status::timeout) {
std::cout << "主线程: 超时!" << std::endl;
} else if (status == std::future_status::deferred) {
std::cout << "主线程: deferred!" << std::endl;
}
wait_until()
: 阻塞当前线程,直到到达指定的时间点。
auto time_point = std::chrono::system_clock::now() + std::chrono::seconds(2);
std::cout << "主线程: 等待 future 准备好 (直到指定时间点)..." << std::endl;
auto status = future.wait_until(time_point);
if (status == std::future_status::ready) {
std::cout << "主线程: future 准备好了!" << std::endl;
} else if (status == std::future_status::timeout) {
std::cout << "主线程: 超时!" << std::endl;
} else if (status == std::future_status::deferred) {
std::cout << "主线程: deferred!" << std::endl;
}
重点:
future.get()
只能调用一次。再次调用会抛出std::future_error
异常。- 可以使用
wait()
、wait_for()
、wait_until()
等方法来控制等待的时间。 future
对象可以用于轮询异步任务的状态,但通常更推荐使用get()
方法阻塞等待结果。
四、std::shared_future
:共享的期望
有时候,我们希望多个线程都能访问异步任务的结果。std::future
对象只能被访问一次,之后就失效了。这时,就需要 std::shared_future
。
-
核心功能: 允许多个线程共享同一个
future
对象,并获取异步任务的结果。 -
使用场景: 当多个线程需要同时访问异步任务的结果时。
-
代码示例:
#include <iostream>
#include <future>
#include <thread>
#include <vector>
int calculate_square(int num, std::promise<int> prom) {
try {
int square = num * num;
prom.set_value(square);
std::cout << "计算线程: 计算 " << num << " 的平方,结果是 " << square << std::endl;
return square;
} catch (const std::exception& e) {
prom.set_exception(std::current_exception());
std::cerr << "计算线程: 发生异常: " << e.what() << std::endl;
return -1;
}
}
void process_result(std::shared_future<int> shared_future, int thread_id) {
try {
int result = shared_future.get();
std::cout << "线程 " << thread_id << ": 收到结果: " << result << std::endl;
} catch (const std::exception& e) {
std::cerr << "线程 " << thread_id << ": 捕获到异常: " << e.what() << std::endl;
}
}
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::thread t(calculate_square, 5, std::move(promise));
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
threads.emplace_back(process_result, shared_future, i + 1); // 创建多个线程,共享 shared_future
}
for (auto& thread : threads) {
thread.join();
}
t.join();
return 0;
}
代码解释:
-
std::shared_future<int> shared_future = future.share();
:将future
对象转换为shared_future
对象。 -
多个线程
process_result
函数都接收同一个shared_future
对象。 -
每个线程都可以通过
shared_future.get()
方法获取异步任务的结果。
重点:
shared_future
可以被多个线程共享。- 每个线程都可以独立地调用
shared_future.get()
获取结果。 - 只有在所有
shared_future
对象都销毁后,底层资源才会被释放。
五、std::packaged_task
:任务打包器
std::packaged_task
就像一个任务打包器,它可以将一个函数或可调用对象打包成一个异步任务,并提供访问任务结果的方式。
-
核心功能: 将函数或可调用对象打包成一个异步任务,并提供一个
future
对象来获取任务结果。 -
使用场景: 当需要将一个已有的函数或可调用对象放到异步线程中执行时。
-
代码示例:
#include <iostream>
#include <future>
#include <thread>
int multiply(int a, int b) {
std::cout << "计算线程: 计算 " << a << " * " << b << std::endl;
return a * b;
}
int main() {
std::packaged_task<int(int, int)> task(multiply); // 将 multiply 函数打包成一个任务
std::future<int> future = task.get_future(); // 获取与任务关联的 future
std::thread t(std::move(task), 6, 7); // 启动线程,执行任务
std::cout << "主线程: 等待计算结果..." << std::endl;
int result = future.get(); // 等待结果
std::cout << "主线程: 收到计算结果: " << result << std::endl;
t.join();
return 0;
}
代码解释:
-
std::packaged_task<int(int, int)> task(multiply);
:创建一个packaged_task
对象,将multiply
函数打包成一个任务。模板参数int(int, int)
指定了函数的签名。 -
std::future<int> future = task.get_future();
:获取与任务关联的future
对象。 -
std::thread t(std::move(task), 6, 7);
:启动线程,执行任务。注意,task
对象通过std::move
传递给线程。同时,函数的参数也传递给线程。
重点:
packaged_task
可以方便地将已有的函数或可调用对象放到异步线程中执行。packaged_task
对象只能被调用一次。packaged_task
可以处理函数抛出的异常,并将异常传递给future
对象。
六、总结:promise
、future
、shared_future
、packaged_task
的区别与联系
为了方便大家理解,我们用一个表格来总结一下这几个概念的区别与联系:
特性 | std::promise |
std::future |
std::shared_future |
std::packaged_task |
---|---|---|---|---|
作用 | 设置异步结果 | 获取异步结果 | 共享异步结果 | 打包异步任务 |
关联对象 | future |
promise |
future |
无 |
线程安全 | 是 | 是 | 是 | 是 |
可设置次数 | 1 | N/A | N/A | N/A |
可获取次数 | 1 | 1 | 多次 | N/A |
所有权 | 唯一 | 唯一 | 共享 | 唯一 |
是否可拷贝 | 否 | 否 | 是 | 否 |
联系:
promise
和future
通常一起使用,promise
负责设置值,future
负责获取值。shared_future
是future
的一种特殊形式,允许多个线程共享结果。packaged_task
内部使用promise
和future
来实现异步任务的封装和结果传递。
七、高级用法:std::async
C++ 标准库还提供了一个方便的函数 std::async
,它可以自动地创建一个异步任务,并返回一个 future
对象。
-
核心功能: 启动一个异步任务,并返回一个
future
对象。 -
使用场景: 当需要快速地启动一个异步任务时。
-
代码示例:
#include <iostream>
#include <future>
int calculate_product(int a, int b) {
std::cout << "计算线程: 计算 " << a << " * " << b << std::endl;
return a * b;
}
int main() {
std::future<int> future = std::async(std::launch::async, calculate_product, 8, 9); // 启动异步任务
std::cout << "主线程: 等待计算结果..." << std::endl;
int result = future.get(); // 等待结果
std::cout << "主线程: 收到计算结果: " << result << std::endl;
return 0;
}
代码解释:
-
std::future<int> future = std::async(std::launch::async, calculate_product, 8, 9);
:使用std::async
函数启动一个异步任务,执行calculate_product
函数。std::launch::async
:指定异步任务在新线程中执行。calculate_product
:要执行的函数。8
和9
:函数的参数。
-
std::async
函数返回一个future
对象,可以用于获取异步任务的结果。
std::launch
的两种策略:
std::launch::async
:强制异步执行,即在新线程中执行。std::launch::deferred
:延迟执行,即在调用future.get()
或future.wait()
时才执行。
如果不指定 std::launch
策略,std::async
会自动选择合适的策略。
八、最佳实践:避免死锁和资源泄漏
在使用 promise
和 future
进行异步编程时,需要注意避免死锁和资源泄漏。
- 避免死锁: 确保在不同的线程中获取锁的顺序一致。避免循环等待。
- 避免资源泄漏: 确保
promise
对象最终会被设置值或异常。可以使用 RAII 封装promise
对象,在对象析构时自动设置一个默认值或异常。
九、总结
std::promise
和 std::future
是 C++ 异步编程的重要工具。它们提供了一种线程安全的、基于承诺的异步结果传递和等待机制。通过合理地使用它们,可以编写出高效、简洁、可靠的多线程程序。
希望今天的课程对大家有所帮助!记住,异步编程虽然复杂,但只要掌握了核心概念和技巧,就能轻松应对各种挑战。 下课!