【C++】异步任务结果传递:promise、future 与 packaged_task
前面介绍了condition_variable解决的是“线程没有任务时如何等待有任务时如何被唤醒”的问题。接下来还有一个很常见的问题子线程执行了一个任务以后任务的返回值怎么交给主线程例如int Add(int a, int b) { return a b; }如果直接创建线程std::thread t(Add, 10, 20);线程当然可以执行Add()但是返回值30怎么拿回来普通std::thread本身不会直接帮我们保存函数返回值。C 提供了std::promise std::future std::packaged_task来解决线程之间的结果传递问题。一、future 可以理解成“未来才能拿到的结果”先从future开始理解。future这个单词本身就是未来所以std::futureint可以理解为现在结果可能还没有计算完成但是未来某个时刻可以通过这个对象拿到一个int。例如std::futureint future;此时future并不直接保存结果它更像是一张“取结果的凭证”。真正获取结果时int result future.get();如果结果已经准备好了get()直接返回。如果结果还没有准备好future.get() ↓ 当前线程等待 ↓ 其他线程完成计算 ↓ 结果准备完成 ↓ get返回结果所以future经常用于主线程提交任务 ↓ 子线程执行任务 ↓ 主线程以后通过future拿结果不过future自己并不能生产结果它需要和std::promise或者std::packaged_task配合使用。二、promise future一个线程设置结果另一个线程获取promise可以理解成我承诺以后会提供一个结果。例如std::promiseint promise;表示未来会产生一个int类型的结果。通过std::futureint future promise.get_future();可以拿到与这个promise对应的future。二者之间的关系可以理解成promise 负责写入结果 ↓ 共享状态 ↑ future 负责读取结果完整示例#include future #include iostream #include thread void Calculate(std::promiseint promise) { int result 10 20; // 将结果设置到共享状态中 promise.set_value(result); } int main() { std::promiseint promise; std::futureint future promise.get_future(); std::thread worker(Calculate, std::move(promise)); // 等待子线程设置结果 int result future.get(); std::cout result result \n; worker.join(); return 0; }输出result 30整个流程是主线程创建promise ↓ 通过get_future得到future ↓ promise交给子线程 ↓ 子线程计算 ↓ promise.set_value(30) ↓ future收到结果 ↓ future.get()返回30这里有一个容易注意到的地方std::thread worker(Calculate, std::move(promise));为什么需要std::move(promise)因为std::promise不能被普通复制。也就是说下面通常不行std::promiseint p1; std::promiseint p2 p1;它只能移动std::promiseint p2 std::move(p1);因为一个promise对应一个共享状态如果随便复制多个promise到底谁负责设置结果就会变得混乱。set_value 是干什么的子线程执行promise.set_value(result);表示结果已经准备完成可以通知 future 了。如果主线程此时正执行future.get();那么它会被唤醒并取得结果。future.get() 只能调用一次例如int result1 future.get(); int result2 future.get();第二次通常会出现错误。因为get()会把共享状态中的结果取走。所以future.get()更像是“领取结果”而不是反复读取变量。如果需要多个地方共享同一个异步结果可以使用std::shared_future不过普通线程池任务返回值大多数情况下std::future已经够用了。三、异常也可以通过 promise 传递异步任务不一定成功。例如int Divide(int a, int b) { if (b 0) { throw std::runtime_error(除数不能为0); } return a / b; }子线程如果直接抛出异常主线程不方便直接得到这个异常。promise可以把异常也保存到共享状态中void Calculate(std::promiseint promise) { try { int result 10 / 0; promise.set_value(result); } catch (...) { promise.set_exception(std::current_exception()); } }主线程try { int result future.get(); } catch (const std::exception e) { std::cout e.what() \n; }完整逻辑子线程正常完成 ↓ promise.set_value() ↓ future.get()得到结果 子线程发生异常 ↓ promise.set_exception() ↓ future.get()重新抛出异常也就是说future不仅可以传递正常返回值也可以把子线程中的异常传递回调用线程。这对于线程池非常重要因为任务执行失败后主线程也需要知道失败原因。四、packaged_task自动把函数返回值放进 future如果每次都手动写std::promiseint promise; std::futureint future promise.get_future();然后在线程函数中promise.set_value(result);会显得比较麻烦。于是 C 提供了std::packaged_task它可以直接包装一个可调用对象并自动把返回值保存到future中。例如int Add(int a, int b) { return a b; }使用packaged_taskstd::packaged_taskint(int, int) task(Add);这里int(int, int)表示这个任务接收两个int参数 返回一个int然后获取结果std::futureint future task.get_future();最后把任务交给线程std::thread worker(std::move(task), 10, 20);完整代码#include future #include iostream #include thread int Add(int a, int b) { return a b; } int main() { std::packaged_taskint(int, int) task(Add); std::futureint future task.get_future(); std::thread worker(std::move(task), 10, 20); int result future.get(); std::cout result result \n; worker.join(); return 0; }输出result 30这里不需要自己写promise.set_value();packaged_task会自动完成执行Add(10, 20) ↓ 得到30 ↓ 把30保存到共享状态 ↓ future.get()得到30所以可以把packaged_task理解成一个能够自动保存函数执行结果的任务包装器。它非常适合线程池。线程池提交任务时通常会把任意函数包装成std::packaged_taskreturn_type()然后把future返回给调用者。例如auto task std::make_sharedstd::packaged_taskint()([]() { return 10 20; }); std::futureint future task-get_future();真正执行任务时(*task)();调用者以后int result future.get();就可以拿到任务结果。这其实就是很多线程池submit()函数的核心思路。五、promise、future、packaged_task 应该怎么理解三者关系可以简单总结为类型主要作用std::promise手动设置结果或异常std::future获取未来产生的结果std::packaged_task包装函数并自动保存返回值promise future更适合手动控制结果产生过程std::promiseint promise; std::futureint future promise.get_future();生产者线程promise.set_value(100);消费者线程int value future.get();而packaged_task future更适合“执行一个函数然后拿到返回值”std::packaged_taskint() task([]() { return 100; }); std::futureint future task.get_future(); task(); int result future.get();在线程池中通常更常见的是用户提交函数 ↓ packaged_task包装函数 ↓ future返回给用户 ↓ task放入任务队列 ↓ 工作线程取出task ↓ 执行task ↓ 返回值自动进入future ↓ 用户future.get()取得结果这也是为什么线程池代码中经常能看到std::future和std::packaged_task一起出现。这一篇最需要掌握的是future负责拿结果 promise负责手动设置结果 promise和future通过共享状态传递数据 future.get()在结果没准备好时会等待 promise可以通过set_exception传递异常 packaged_task可以自动保存函数返回值 线程池通常使用packaged_task包装任务 再把future返回给任务提交者。0voice · GitHub