C++11 扩展 --- 并发支持库(补充3)

C++11 扩展 --- 并发支持库(补充3) C扩展 --- 并发支持库补充2https://blog.csdn.net/Small_entreprene/article/details/149854780?spm1001.2014.3001.5501在多线程编程中我们经常需要从线程中获取任务执行的结果。比如启动一个线程压缩文件后我们需要知道压缩后的文件名和大小。在 C11 之前获取线程返回值并不直观通常需要借助指针、互斥锁和条件变量的组合代码复杂且容易出错。传统方式的痛点先看看没有future库时我们是如何获取线程返回值的#includeiostream #includethread #includemutex void fun(int x, int y, int* ans) { *ans x y; } int main() { int a 10; int b 8; int* sum new int(0); std::thread t(fun, a, b, sum); t.join(); // 获取线程的返回值 std::cout *sum std::endl; // 输出18 delete sum; return 0; }这段代码虽然能工作但存在明显问题需要手动管理动态内存new/delete必须显式调用join()等待线程完成无法优雅地处理多个返回值没有异常处理机制如果线程需要在不同阶段返回多个结果代码会变得更加复杂需要更多的同步机制来协调线程间的数据传递。什么是 future 库C11 引入的future头文件彻底改变了传统线程获取返回值的复杂方式它提供了一套现代化的异步编程机制。其中std::future是一个类模板class template专门用于接收异步任务的返回值或异常是 C 异步编程的核心组件。简单来说std::future就像是一张类型安全的提货单当我们通过std::async、std::packaged_task或std::promise启动一个异步任务时系统会立刻返回一个与任务返回值类型匹配的futureT对象我们不需要立即等待任务结束而是可以在后续需要结果时调用future.get()来获取返回值。如果任务尚未完成get()会阻塞等待如果任务已完成则直接返回结果若任务抛出异常get()也会将异常抛出让主线程可以安全捕获实现了异步操作、结果获取与异常处理的一体化比手动管理线程、共享内存和同步机制更简洁、更安全。【核心要点】std::future只能调用一次get()第一次调用后共享状态就会被释放再次get()会导致未定义行为想让多个线程获取结果要用std::shared_future。【注意】【所以我们应该按需保存】future 的核心组件future库主要包含以下几个核心组件std::future获取异步操作结果的 提货单std::async启动异步任务的便捷函数std::promise用于主动设置异步操作结果std::packaged_task封装可调用对象为异步任务下面我们逐一了解这些组件的用法。1. std::async最简单的异步任务std::async是启动异步任务最简便的方式它会自动创建线程并运行任务返回一个std::future对象供我们获取结果。#include iostream #include thread #include future #include chrono int add(int x, int y) { std::cout [add] : my thread id is std::this_thread::get_id() std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟业务处理时间 return x y; } int main() { std::cout [main] : my thread id is std::this_thread::get_id() std::endl; std::cout 使用 async 开启一个处理任务, 返回 future 封装好的结果 std::endl; int x, y; std::cout 请输入要计算的数: std::endl; std::cout x ; std::cin x; std::cout y ; std::cin y; std::futureint result std::async(add, x, y); std::cout 等待结果中... std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout 计算结果: result.get() std::endl; return 0; }lfzU22:~/WorkSpace/myLearn/future$ ./main [main] : my thread id is 127777395471296 使用 async 开启一个处理任务, 返回 future 封装好的结果 请输入要计算的数: x 100 y 230 等待结果中... [add] : my thread id is 127777388885568 计算结果: 330这段代码比传统方式简洁得多我们不需要手动创建线程、管理内存也不需要显式调用join()。所以 std::async是 C11 用来启动一个异步任务的函数你给它传一个函数 / 可调用对象它就会在后台自动运行不用你自己创建 thread、不用 join、不用管线程生命周期。它会直接返回一个std::future你后面用future.get()就能拿到返回值。那为什么不需要主线程进行join呢因为std::async会返回一个std::future对象而等待线程结束、获取返回值的逻辑已经被封装在future.get()内部当我们调用get()时如果【异步任务】所在线程还没有执行完毕get()会自动阻塞当前主线程一直等待线程运行完成这个行为本质上和std::thread的join()是一样的相当于系统在底层帮我们自动完成了等待线程结束的操作不需要我们再手动调用join()而如果不调用get()只要future对象正常生命周期结束相关的线程资源也会被合理管理不会像直接使用std::thread忘记join()那样触发程序异常所以使用std::async时主线程不再需要显式写join()。启动策略std::async提供了两种启动策略std::launch::async立即创建新线程执行任务std::launch::deferred延迟执行直到调用 get () 时才在当前线程执行#include iostream #include thread #include future #include chrono #include sstream #include string std::string showInfo(const std::string name) { // 获取线程 ID std::thread::id tid std::this_thread::get_id(); // 转成 string std::ostringstream oss; oss tid; std::string tid_str oss.str(); std::string idInfo [ name ] 线程 ID: tid_str; return idInfo; } int main() { std::cout 使用 async 的不同策略开启一个处理任务 std::endl; std::future task1 std::async(std::launch::async, showInfo, 异步任务); std::string deferred_msg 延迟任务; std::future task2 std::async(std::launch::deferred, showInfo, std::ref(deferred_msg)); std::cout [main] : 线程 ID: std::this_thread::get_id() std::endl; std::cout 等待结果中... std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout task1.get() std::endl; std::cout task2.get() std::endl; return 0; } 使用 async 的不同策略开启一个处理任务 [main] : 线程 ID: 124725928223680 等待结果中... [异步任务] 线程 ID: 124725921576512 [延迟任务] 线程 ID: 124725928223680std::async存在两种关键启动策略会直接影响任务执行方式std::launch::async表示立即创建新线程并在后台异步执行任务属于真正的异步std::launch::deferred则不会创建线程只会延迟执行直到调用get()或wait()时才在当前线程同步运行函数并非异步而默认不指定策略时等效于同时使用两种策略由系统自行选择行为不确定是容易踩坑的地方。同时无论采用哪种启动策略我们都不需要手动调用join()因为使用std::launch::async时get()内部会自动阻塞等待线程结束相当于完成了join的工作使用std::launch::deferred时则直接在当前线程执行根本不存在线程需要等待的问题因此std::async结合std::future彻底省去了手动管理线程和join的操作。2. std::promise主动设置结果在 C11 引入的并发编程模型中std::promise与std::future是一对相辅相成的工具它们共同构建了异步操作中 **“结果传递”的桥梁。简单来说std::promise负责 “生产”结果而std::future负责“消费”结果二者通过一个隐藏的“共享状态” 实现跨线程通信。具体来说std::promise 和 std::future 本质上是一对一的生产者 - 消费者模型一个 promise 只能对应一个 future不支持一对多、多对多所以不存在我们担心的 “多个生产覆盖数据”“多个线程重复消费” 的问题因为标准库设计上就限制了只能写一次、只能读一次。它的底层共享状态需要保证生产者写入后消费者能被精准唤醒并且等待时不占用 CPU所以实现上不会用轻量的原子 CAS而是使用互斥锁保护共享状态条件变量实现等待与唤醒这样既能保证线程安全又能让消费者在没有结果时进入休眠避免空轮询是最稳定、最符合 C 标准要求的实现方式。虽然std::promise和std::future是一对一、只写一次、只读一次从逻辑上避免了多写、重复消费但它们之间的共享状态不是原子操作且future的阻塞等待必须靠条件变量实现而条件变量本身强制依赖互斥锁才能安全等待与唤醒所以底层依然必须用锁锁是为了保证共享状态读写完整、等待唤醒机制可靠不是为了防并发覆盖。先看一个基础示例感受它们的协作方式#include iostream #include thread #include future int main() { std::cout promise 与 future 生产消费关系 std::endl; std::promiseint producer; // 绑定生产方 std::futureint consumer producer.get_future(); std::thread t([producer](){ // 模拟耗时操作如网络请求、文件IO等 std::this_thread::sleep_for(std::chrono::seconds(1)); // 任务完成后通过promise设置结果触发共享状态就绪[就是生产产品,通知消费] producer.set_value(42); }); // 主线程中future 等待结果就绪并获取 std::cout 等待异步结果... std::endl; // get()会阻塞直到结果可用且只能调用一次[因为绑定方只生产了一个产品] std::cout 获取到的结果: consumer.get() std::endl; // std::cout 获取到的结果: consumer.get() std::endl; // 再次就会报错 t.join(); return 0; }lfzU22:~/WorkSpace/myLearn/future$ ./main promise 与 future 生产消费关系 等待异步结果... 获取到的结果: 42std::promise的独特价值std::promise最强大的特性在于其 主动性—— 它允许我们在任意时机、任意线程中设置结果而非局限于异步任务本身。这种灵活性使其适用于多种场景分阶段任务例如一个线程负责预处理数据另一个线程在预处理完成后通过promise设置中间结果主线程则通过future获取并继续处理。异常传递当异步操作发生错误时可通过promise.set_exception()将异常存储到共享状态future.get()会在主线程中重新抛出该异常实现跨线程异常安全传递。外部事件触发比如等待用户输入、信号量触发等外部事件完成后通过promise手动标记结果就绪。与std::future的核心区别虽然二者总是成对出现但职责边界清晰维度std::promisestd::future角色结果的 生产者结果的 消费者核心操作set_value()/set_exception()设置结果get()/wait()获取 / 等待结果状态影响主动将共享状态置为 就绪被动等待共享状态变为 就绪生命周期通常与生产者线程绑定通常与消费者线程绑定复制性不可复制仅可移动确保结果唯一设置不可复制仅可移动结果只能被获取一次需要注意的是future.get()是一次性操作 —— 一旦调用共享状态的结果会被 取走再次调用将导致未定义行为。如果需要在多个线程中获取同一结果可使用std::shared_future通过future.share()转换它支持多次获取结果。其实到这里我们就可以这么去理解std::promise std::future 本质上就是封装好的、绑定一对一的条件变量 互斥锁 共享状态。future.get()没数据时就是condition_variable.wait()阻塞休眠、不占 CPUpromise.set_value()就是把值写进共享状态然后自动调用notify_one()精准唤醒绑定的那个 future它和你手写mutex condition_variable的底层逻辑一模一样只是 C 把 “等待、唤醒、共享状态、锁” 全部打包好了你不用自己写更安全、更少出错。一句话future 封装好的 waitpromise 封装好的 notify_one它们就是语法糖版、安全版、一对一版的条件变量。扩展与其他异步工具的配合std::promise与std::future是 C 异步编程的基础它们还能与更高层次的工具配合使用std::packaged_task将函数或可调用对象包装为一个 任务自动创建关联的future本质是对promise的封装任务执行完毕后自动调用set_value。std::async更简洁的异步接口可直接启动异步任务并返回future无需手动管理线程和promise底层可能使用线程池优化性能。例如用std::async简化上述示例// 自动管理线程返回的future直接关联任务结果 std::futureint fut std::async(std::launch::async, []() { std::this_thread::sleep_for(std::chrono::seconds(1)); return 42; }); std::cout 结果: fut.get() std::endl;综上std::promise与std::future构成了 C 异步编程的 结果通道前者赋予我们主动控制结果的能力后者则提供了安全获取结果的机制。理解这对工具的协作模式是掌握 C 并发编程的重要一步。3. std::packaged_task封装任务std::packaged_task是 C11 引入的异步编程工具它能将可调用对象函数、lambda、函数对象等包装起来自动创建关联的std::future当任务执行完毕后结果会自动存储到共享状态中供future获取。也就是说std::packaged_task就是封装了promise的任务包装器它内部自动创建promise、自动关联future我们只需要给它一个函数它执行完函数后自动把返回值塞给 promise我们通过future.get()拿结果就行。基本语法结构// 声明包装返回类型为T参数类型为Args...的可调用对象 std::packaged_taskT(Args...) task(可调用对象); // 获取关联的future std::futureT fut task.get_future(); // 执行任务两种方式 task(args...); // 直接在当前线程执行 std::thread t(std::move(task), args...); // 转移到新线程执行#include iostream #include future #include cmath #include thread // 计算平方根的函数 double compute_square_root(double x) { return std::sqrt(x); } int main() { // 封装任务 std::packaged_taskdouble(double) task(compute_square_root); // 获取future对象 std::futuredouble result task.get_future(); // 在新线程中执行任务 std::thread th(std::move(task), 25.0); // 获取结果 std::cout 25的平方根是: result.get() std::endl; th.join(); return 0; }创建 packaged_task把函数compute_square_root包进去内部自动生成一个 promise。获取 futuretask.get_future()拿到和内部 promise 绑定的 future。把任务扔到线程执行线程启动后运行函数计算出结果。自动设置结果函数返回值自动被 packaged_task 调用promise.set_value(返回值)。主线程 get ()阻塞等待 → 拿到结果。join 等待线程结束。std::packaged_task非常适合那些需要重复执行的任务我们可以像传递普通函数一样传递它。future 的常用方法std::future提供了一系列方法来管理和获取异步操作的结果get()获取结果如果任务未完成则阻塞等待wait()等待任务完成但不获取结果wait_for(duration)等待指定时长返回等待状态wait_until(timepoint)等待到指定时间点返回等待状态valid()检查 future 是否有效是否关联到一个异步任务#include iostream #include future #include chrono int long_task() { std::this_thread::sleep_for(std::chrono::seconds(3)); return 42; } int main() { auto fut std::async(long_task); // 等待最多1秒 auto status fut.wait_for(std::chrono::seconds(1)); if (status std::future_status::ready) { std::cout 任务已完成结果: fut.get() std::endl; } else if (status std::future_status::timeout) { std::cout 等待超时任务仍在执行... std::endl; } else if (status std::future_status::deferred) { std::cout 任务被延迟执行 std::endl; } // 最终还是要获取结果 std::cout 最终结果: fut.get() std::endl; return 0; }异常处理异步任务中抛出的异常会被 future 捕获当调用get()时会重新抛出让我们可以在主线程中统一处理异常#include iostream #include future #include stdexcept void risky_operation() { // 模拟一个可能失败的操作 throw std::runtime_error(操作失败: 资源不足); } int main() { std::futurevoid fut std::async(risky_operation); try { fut.get(); // 可能会抛出异常 } catch (const std::exception e) { // 在主线程中处理异常 std::cout 捕获到异常: e.what() std::endl; } return 0; }这种机制确保了异步任务的异常不会被忽略并且可以按照我们熟悉的方式处理。总结future库为 C 异步编程提供了强大而优雅的解决方案简化了线程返回值的获取方式无需手动管理同步机制提供了多种异步任务的创建方式async、promise、packaged_task内置了灵活的等待机制和异常处理让代码更加清晰、简洁、易于维护从传统的线程 指针 锁的复杂组合到使用 future 库的简洁代码C 的异步编程体验得到了质的飞跃。掌握 future 库能让你在多线程编程中更加得心应手编写出更高质量的并发代码。在实际开发中我们可以根据具体需求选择合适的组件简单异步任务用std::async需要主动设置结果用std::promise封装可重用任务用std::packaged_task。