ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++11中std::async的使用详解

C++11中std::async的使用详解 前言std::async是 C11 引入的高层并发接口它想解决的是std::thread用起来太底层的问题用std::thread你得自己管线程的生命周期join还是detach、自己想办法把返回值传回来、自己把异常从工作线程搬到调用线程。std::async把这些包成了一个动作——把这个函数丢出去跑给我一张取结果的凭据凭据就是std::future异常会在get()的地方被重新抛出。关于它有三个高频误解第一以为std::async一定会开新线程。默认启动策略是std::launch::async | std::launch::deferred也就是说可以异步执行也可以延迟执行由实现挑一个。如果它挑了延迟执行deferred函数根本不会在后台跑而是等到你调用get()或wait()的那一刻在调用线程上执行。第二以为丢掉返回值只是浪费了一个 future。对launch::async启动的任务future的析构会阻塞到任务完成——所以std::async(std::launch::async, f);这种写法会让代码看起来并发、实际完全串行。第三以为future::get()可以反复调用。get()是取走语义调用一次后valid()变成false再调用是未定义行为UB。想多方读取要用std::shared_future。本文按启动策略 → future 语义 → 异常传播与实战 → 坑点的顺序讲。代码以 C17 为基准用到的都是 C11 就有的设施在 GCC 13 / Clang 17 / MSVC 19.3x 上均可编译GCC/Clang 需要加-pthread。一、两种重载与启动策略std::async有两个重载区别就在于第一个参数是不是启动策略#include future int work(int a, int b); // 重载 1不接受策略参数等价于传入 std::launch::async | std::launch::deferred auto f1 std::async(work, 1, 2); // 重载 2第一个参数是启动策略 auto f2 std::async(std::launch::async, work, 1, 2); auto f3 std::async(std::launch::deferred, work, 1, 2);两个重载的返回类型都是std::futureRR是用给定实参调用该可调用对象的结果类型C17 起按std::invoke_result推导它之前的名字是std::result_of。推导规则以及参数按值退化拷贝的行为和std::thread的构造函数完全一致——这一点很关键。启动策略std::launch是一个位掩码风格的枚举可以用|组合的语义差别策略执行线程何时开始执行wait_for的返回值典型用途std::launch::async新线程实现可以选择复用不保证立即ready/timeout真正要并行std::launch::deferred调用get()/wait()的那个线程惰性第一次get()/wait()时才做立即返回deferred延迟求值两者按位或默认由实现选由实现选视实现选择而定不推荐行为不可预期std::launch::deferred有一个很有用的性质如果从头到尾没人get()/wait()这个函数就永远不执行。这可以拿来做惰性求值但也正是异常会被静默丢弃的原因。反过来launch::async有一个必须知道的副作用它的future析构会阻塞。C11 起的标准规定由std::async创建、策略包含launch::async、且尚未就绪的那些future最后一个被销毁时会阻塞到任务完成——这就是丢掉返回值就变成串行的确切原因。另外标准并不要求std::async复用线程。以 libstdc 的实现为例实现细节不是标准规定launch::async下每次调用都会新建一个std::thread所以它不适合替代线程池。std::async和手写std::thread的对比大致是维度std::threadstd::async必须做的收尾析构前必须join()或detach()否则std::terminate不用future析构会处理可能阻塞取返回值得自己用引用/指针参数带出来容易出竞态future::get()直接返回异常传播线程里未捕获的异常直接std::terminate捕获并存入共享状态get()时重抛何时开始执行一定立即取决于启动策略可能永不执行参数传递按值拷贝引用要用std::ref同上线程复用无一个thread一个线程无保证libstdc 也是每次新建二、std::future的语义一次取值、两种查询std::future是一个只移动、不可拷贝的句柄代表一份尚未就绪的共享状态。它的核心成员成员作用备注get()阻塞到就绪取走值或重抛异常调用后valid()变false再调是 UBwait()阻塞到就绪不取值可以多次调用wait_for(dur)最多等dur返回ready/timeout/deferredwait_until(tp)等到时间点同上valid()是否还关联着共享状态默认构造的future是falseshare()转成std::shared_future转完之后原future不再 validstd::shared_future才是可以多方读取的版本它的get()返回const T对shared_futurevoid返回void可以调用任意多次。#include future #include iostream int compute() { return 42; } int main() { std::futureint f std::async(std::launch::async, compute); std::cout valid before get: std::boolalpha f.valid() \n; // true std::cout f.get() \n; // 42 std::cout valid after get: f.valid() \n; // false // f.get(); // ❌ UBvalid() 为 false 时调用 get()标准不保证任何行为 std::shared_futureint sf std::async(std::launch::async, compute).share(); std::cout sf.get() sf.get() \n; // 42 42可以重复取 return 0; }注意.share()那行std::async(...)返回的是临时futureshare()之后它随即析构但共享状态已转移给sf不会被提前销毁。wait_for是我不想无限等时用的。很多人把它当bool用但std::future_status是enum class没有到bool的隐式转换直接写if (f.wait_for(...))会编译失败。必须显式比较#include chrono #include future #include iostream int slow_task() { return 7; } int main() { std::futureint f std::async(std::launch::async, slow_task); // ❌ if (f.wait_for(std::chrono::milliseconds(10))) { } // 编译错误 // ✅ 显式比较状态 if (f.wait_for(std::chrono::milliseconds(10)) std::future_status::ready) { std::cout ready: f.get() \n; } else { std::cout still running\n; std::cout f.get() \n; // 后面再取也没问题get 会继续阻塞 } return 0; }另一个必须知道的语义带延迟启动的future在wait_for上会立刻返回std::future_status::deferred因为它压根就没开始跑。三、异常传播、参数传递与实战3.1 异常怎么从工作线程搬回来如果可调用对象抛出异常std::async会把异常对象拷贝进共享状态调用get()时它会在调用get()的线程上被重新抛出。如果你从不调用get()也不wait()这个异常就被静默丢弃了。#include future #include iostream #include stdexcept #include string int parse(const std::string s) { if (s.empty()) throw std::invalid_argument(empty input); return static_castint(s.size()); } int main() { std::futureint f1 std::async(std::launch::async, parse, std::string(hello)); std::cout f1.get() \n; // 5 std::futureint f2 std::async(std::launch::async, parse, std::string()); try { std::cout f2.get() \n; // 在这里重抛 std::invalid_argument } catch (const std::invalid_argument e) { std::cout caught: e.what() \n; // caught: empty input } // ⚠️ 异常被丢弃f3 从未被 get/wait异常在 future 析构时无声消失 std::futureint f3 std::async(std::launch::async, parse, std::string()); return 0; }3.2 参数是按值退化拷贝的和std::thread一样std::async会把实参按值存进共享状态数组退化为指针、函数退化为函数指针、引用被拷贝掉一份。想传引用必须包一层std::ref/std::cref而一旦用了引用就得回答它能不能活到任务结束。#include cstddef #include future #include iostream #include vector int main() { std::vectorint data(100, 3); // 区间求和按 const 接收避免拷贝整个 vector auto sum_range [](const std::vectorint v, std::size_t lo, std::size_t hi) - long long { long long s 0; for (std::size_t i lo; i hi; i) s static_castlong long(v[i]); return s; }; // ✅ 用 std::cref 包一下任务里拿到的才是指向原 data 的 const 引用 // 否则 data 会被整个拷贝进共享状态 std::futurelong long left std::async( std::launch::async, sum_range, std::cref(data), std::size_t{0}, std::size_t{50}); std::futurelong long right std::async( std::launch::async, sum_range, std::cref(data), std::size_t{50}, std::size_t{100}); const long long total left.get() right.get(); std::cout total total \n; // 300 return 0; }3.3 一次并发但不是的翻车现场下面这段没有计时断言只把行为摆出来可以自己编译运行观察输出顺序#include chrono #include future #include iostream #include thread int slow(int id) { std::this_thread::sleep_for(std::chrono::milliseconds(200)); return id * 2; } int main() { std::cout A: 丢弃返回值\n; std::async(std::launch::async, slow, 1); // ❌ 临时 future 在此句末尾析构 → 阻塞 std::cout A: 这一行会在任务跑完之后才打印\n; // 实际已经是串行 std::cout B: 保存 future\n; std::futureint f1 std::async(std::launch::async, slow, 1); std::futureint f2 std::async(std::launch::async, slow, 2); std::cout B: 两个任务已经在并行跑了结果是 f1.get() f2.get() \n; return 0; }编译命令g -stdc17 -Wall -Wextra -pthread async_demo.cpp -o async_demo常见坑点坑 1不保存std::async的返回值。// ❌ 临时 future 在语句末尾析构析构阻塞到任务完成 —— 看起来并发实际串行 std::async(std::launch::async, do_work); // ✅ 保存下来需要时再取 std::futurevoid f std::async(std::launch::async, do_work); f.get();坑 2默认启动策略导致函数压根没执行。实现可能选择延迟执行此时若你从不调用get()/wait()函数体一次都不会运行里面的异常也一并消失。// ❌ 依赖默认策略可能在没人取结果时永远不执行 std::async(log_to_file, boot); // ✅ 要后台执行就显式写 async用不到时也应显式写 deferred 表明意图 std::futurevoid f std::async(std::launch::async, log_to_file, boot); f.wait();坑 3误读async | deferred。它不是两个都做一遍而是两种都允许实现自己挑一个写成std::async(std::launch::async | std::launch::deferred, work)并不会同时拿到并行和惰性两样好处想要确定的行为就只指定一种例如std::async(std::launch::async, work)。坑 4get()调用两次。std::futureint f std::async(std::launch::async, compute); int a f.get(); // ❌ UB此时 f.valid() 为 false标准不保证任何行为 // int b f.get(); // ✅ 要多次读取就用 shared_future std::shared_futureint sf std::async(std::launch::async, compute); int x sf.get(); int y sf.get(); // OK坑 5把std::ref指向的局部变量带出了作用域。#include future // ❌ 返回的 future 持有对 local 的引用函数一返回 local 就没了 // 后台线程稍后写它 → 悬垂引用UB std::futurevoid bad() { int local 0; return std::async(std::launch::async, [](int x) { x 1; }, std::ref(local)); } // ✅ 让任务自己持有数据的所有权按值传参或把生命周期提升到调用方 std::futureint good() { return std::async(std::launch::async, [] { return 1; }); // 无外部引用 }用std::ref的判据很简单这个被引用的对象能不能保证活到get()/wait()返回不确定就不要用引用。坑 6在future上既没get()也没wait()异常静默丢失。// ❌ 任务抛出的异常没人接程序静默继续 std::async(std::launch::async, might_throw); // ✅ 至少 get 一次让异常被重新抛出 std::futurevoid f std::async(std::launch::async, might_throw); f.get();坑 7把std::async当线程池用。标准没有复用线程的语义libstdc 的实现就是每次launch::async新建一个std::thread高频调用时线程创建销毁的开销会占大头。复用与否属于实现细节但不要假定它复用线程是可移植的结论。// ❌ 一万次小任务 → 一万次可能的线程创建 for (int i 0; i 10000; i) { futs.push_back(std::async(std::launch::async, tiny_task, i)); } // ✅ 要么改用固定大小的线程池要么把任务合并成少量较大的块坑 8把future当作保证任务在后台跑的凭据。future只保证结果可取不保证现在正在跑能观察的只有wait_for返回的状态。auto f std::async(std::launch::deferred, work); // 注意这里是 deferred // f.wait_for(...) 会立刻返回 std::future_status::deferred // 这意味着任务根本没开始而不是任务还没完成总结要点结论返回类型std::futureRR是调用结果类型C17 起按std::invoke_result推导启动策略async立即在新线程跑deferred惰性、在get()/wait()的线程上跑默认是两者按位或默认策略的风险行为由实现选择可能永不执行、可能丢异常、可能阻塞future析构对launch::async且未就绪的任务会阻塞C11 起的标准要求——所以别丢弃返回值get()只能调一次调完valid()变false再调是 UB多方读取用shared_futurewait_for返回std::future_statusenum class不能当bool用延迟任务立刻返回deferred异常存入共享状态在get()处重抛从不get()就静默丢弃参数传递与std::thread一致按值退化拷贝要引用用std::ref/std::cref线程复用标准不保证libstdc 的实现是每次新建线程实现细节三句话收尾显式写清楚启动策略、永远保存future并在确定的时点get()既避免析构阻塞把并发变成串行也避免异常被吞掉、用std::ref之前先回答它活得够久吗。做到这三点std::async就是非常好用的小工具把它当线程池用则是它设计上不打算承担的事。
返回列表