std::experimental::when_any 深度解析:等待“第一个完成”的异步组合原语

在 C++ 并发编程里,std::future::get()是"等一个结果",when_all是"等所有结果",而 std::experimental::when_any解决的是第三类问题:一组异步任务里,谁先就绪就先响应谁 。它来自 C++ Extensions for Concurrency(Concurrency TS),配合带延续(continuation)的 std::experimental::future使用,是"竞速执行 / 投机执行 / 超时控制 / 并行搜索"的核心组合算子。


一、它到底解决什么问题

传统写法要"等第一个完成",通常是:

  • 起 N 个线程

  • 用互斥量 + 条件变量抢第一个结果

  • 再想办法取消或忽略其余任务

这套写法又重又容易写错。when_any的语义非常干净:

输入一组 future<T>/ shared_future<T>,返回一个新 future只要其中任意一个输入就绪(有值或有异常),返回的 future 就就绪

它不等所有人,也不阻塞式轮询,而是把"竞争完成"这件事变成一等公民。

典型场景包括:

  • 多镜像源下载,取最快那个

  • 并行搜索大数组,找到一个就处理

  • 主任务 + 定时器竞速,实现异步超时

  • 多个模型/算法同时跑,先出结果先采用(speculative execution)


二、两种重载:变参 vs 迭代器区间

1. 变参版本(编译期已知个数)

复制代码
template<class... Futures>
future<when_any_result<tuple<Futures...>>>
when_any(Futures&&... futures);
  • 每个参数可以是 future<T>shared_future<T>

  • 类型可以不同,结果用 std::tuple保存

  • future被移动,调用后原 future 失效

  • shared_future被复制,原 shared_future 仍有效

2. 迭代器区间版本(运行期未知个数)

复制代码
template<class InputIt>
future<when_any_result<vector<typename InputIt::value_type>>>
when_any(InputIt first, InputIt last);
  • 输入必须是同类型的 future<R>shared_future<R>

  • 结果用 std::vector保存

  • 空区间(first == last)会返回一个立即就绪的 future,index = size_t(-1),容器为空


三、返回类型:when_any_result 是什么

when_any不是直接把"胜者的值"吐出来,而是返回一个包裹结构:

复制代码
template<class Sequence>
struct when_any_result {
    std::size_t index;
    Sequence futures;
};
  • index:第一个就绪的 future 在输入序列中的位置

  • futures:原输入 future 的集合(顺序不变)

    • 变参版本是 tuple

    • 区间版本是 vector

也就是说,when_any的返回值是:

复制代码
future<when_any_result<tuple<future<int>, future<string>>>>
// 或
future<when_any_result<vector<future<int>>>>

这一点和 when_all很像,但语义不同:when_all关心"全部完成后的集合",when_any关心"谁先完成 + 原始集合还在不在"。


四、future 与 shared_future 的行为差异

when_any对两种输入的处理方式不同:

输入类型 调用后原对象 结果容器里
future<T> valid() == false future 被移动进去
shared_future<T> 仍有效 shared_future 被复制进去

所以:

  • 如果你想"多个消费者共享结果",用 shared_future

  • 如果你做完 when_any就不再单独等某个任务,用 futurestd::move进去

错误示例:

复制代码
std::experimental::future<int> f = async([] { return 42; });
when_any(f);          // 错误:future 不能拷贝
when_any(std::move(f)); // 正确

五、和 .then() 搭配才是现代写法

Concurrency TS 的 future支持延续:

复制代码
std::experimental::future<int> a = async([] { return computeA(); });
std::experimental::future<int> b = async([] { return computeB(); });

auto any_f = when_any(std::move(a), std::move(b));

any_f.then([](auto f) {
    auto result = f.get();               // when_any_result<tuple<future<int>, future<int>>>
    std::size_t idx = result.index;

    if (idx == 0) {
        int v = std::get<0>(result.futures).get();
    } else {
        int v = std::get<1>(result.futures).get();
    }
});

关键点:

  • when_any本身不阻塞

  • 返回的 future 就绪后,通过 .then()在"某个实现定义的上下文"里继续跑

  • 胜者已经就绪,所以 result.futures里对应位置的 .get()不会阻塞

  • 其余 future 还在容器里,你可以继续等,也可以直接丢弃


六、经典场景 1:异步超时

复制代码
std::experimental::future<Data> task = async(do_expensive_work);
std::experimental::future<void> timer = async(sleep_for, 100ms);

when_any(std::move(task), std::move(timer))
.then([](auto r) {
    if (r.index == 1) {
        // 定时器先就绪:超时
    } else {
        // 任务先完成:拿结果
        auto data = std::get<0>(r.futures).get();
    }
});

这就是"async timeout"的标准范式:不需要线程取消机制,也能实现"超过 N ms 就放弃"。


七、经典场景 2:并行搜索,找到一个就处理

把数据分块,每块一个 future:

复制代码
std::vector<std::experimental::future<Result>> tasks;
for (auto&& chunk : chunks) {
    tasks.push_back(async([&] { return search(chunk); }));
}

std::experimental::future<when_any_result<vector<future<Result>>>> f =
    when_any(tasks.begin(), tasks.end());

f.then([](auto r) {
    auto result = r.get();
    Result hit = std::get<result.index>(result.futures).get();
    process(hit);
});

注意:这里只解决"第一个命中可处理",不自动取消其余任务。真要取消,需要:

  • 原子标志 stop_requested

  • 任务里周期性检查

  • 或者用协程 / 执行器级 cancellation

when_any只负责"感知第一个完成",不负责"杀掉别人"。


八、when_any 与 when_all 的本质区别

维度 when_all when_any
触发条件 全部就绪 任意一个就绪
返回内容 原 future 集合 index + 原 future 集合
适用模型 join / 汇总 choice / 竞速
异常处理 各子 future 自己抛 子 future 可能带异常,"胜者"可能是异常
典型用法 多阶段流水线收尾 多源取最快、超时、投机执行

一个容易忽略的点:"胜者"不一定是成功结果 。如果第一个就绪的 future 是 exceptional 状态,when_any的返回 future 仍然会就绪,但你在 result.futures[index].get()时会拿到异常。


九、空输入与边界行为

  • 变参版本不传参数:返回立即就绪的 future<when_any_result<tuple<>>>index = size_t(-1)

  • 区间版本 first == last:返回立即就绪 的 vector 版本,index = size_t(-1)

  • 输入里有无效 future:undefined behavior

  • 输入类型不一致(区间版本):编译失败;变参版本允许异构


十、为什么标准最终没把 when_any 放进 std?

Concurrency TS 里的 experimental::future+ then+ when_any/when_all是一套"未来异步模型"提案,但后来 C++ 的演化重心转向了:

  • std::async保持基础能力

  • Executors / P2300 std::execution

  • C++20 协程 co_await

  • when_any风格能力在 sender/receiver 里变成 let_first / when_any类算法

所以今天你会在三处看到"when_any 精神":

  1. std::experimental::when_any(历史 TS,部分库实现过)

  2. P2300 / stdexec:when_any在 sender 世界重新出现

  3. 协程里:co_await select(a, b)/ 自写 race 逻辑


十一、现代替代写法(C++20 协程思路)

如果不用 experimental,用协程可以写成:

复制代码
task<int> race(task<int> a, task<int> b) {
    auto ra = a;
    auto rb = b;
    co_await whatever_ready(ra, rb);
    if (ra.done()) co_return co_await ra;
    co_return co_await rb;
}

但底层语义和 when_any一致:竞争就绪,不是全部完成


十二、一句话总结

std::experimental::when_any是"异步世界里的 select / race / first-completed-wins 原语":它把一组 future 收成一个新 future,第一个就绪就触发,返回"索引 + 原集合",本身不阻塞、不取消其他任务、不隐藏异常。会用 when_any,才算真正从"多线程"走到"异步组合"。

相关推荐
Rabitebla2 小时前
【Linux系统编程】指令(三):创建、删除、复制、移动、看内容
linux·数据结构·c++·算法
程序员老陆2 小时前
CMake 常用关键字与命令全景:从 set到 target_link_options的现代工程视角
c++·cmake·程序开发
码匠许师傅3 小时前
【C++三方组件】cxxopts:轻量首选的命令行解析
c++
hansang_IR3 小时前
【讲解】CSP-S2026 第一轮初赛
c++·算法
Methy3 小时前
IoTHub半个月崩了五次?都是裸rethrow惹的祸
服务器·c++·后端
我不会起名字3223 小时前
一天一道力扣Hot100(36):深度优先算法---组合总和
数据结构·c++·后端·python·算法·go
纪念 2293 小时前
c++类和对象(四)
开发语言·c++
汉克老师3 小时前
GESP2026年9月认证C++六级( 第三部分编程题(2、分树规划))精讲
c++·gesp·小学生·学c++编程
学生小羊3 小时前
C++ 初阶 学习博客
c语言·c++·c++与c语言的区别·c++基础学习