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就不再单独等某个任务,用 future并 std::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,才算真正从"多线程"走到"异步组合"。

相关推荐
程序员老陆9 小时前
C++ 多线程通信方式全景:从共享内存到结构化协调
开发语言·c++
某不知名網友9 小时前
ROS2 核心通信:话题发布与订阅详解
开发语言·c++
C++ 老炮儿的技术栈9 小时前
char (*csConnectName)[256]; 和 char csConnectName [256] 有什么区别
java·c语言·开发语言·c++·人工智能·算法·c
大鹏说大话9 小时前
C++ STL 容器怎么选?vector、list、map、unordered_map 对比
开发语言·c++·list
JMchen9 小时前
性能优化——让Native代码飞起来
android·c++·性能优化
chenbingjie_c10 小时前
手撕 C++ priority_queue:二叉堆 + 模板仿函数 + 模板特化(保姆级逐函数拆解)
开发语言·c++
小小龙学IT10 小时前
C++ Redis 客户端深度解析(hiredis 与 redis-plus-plus)——工业数采实时缓存 C++ 侧实战
c++·redis·缓存
垆边人似月.10 小时前
华为机试题 :最长递增子序列题目
数据结构·c++·算法·华为
开发者联盟league11 小时前
c++11 使用chrono库转换时间点和字符串,计算时间点间隔
java·c++·算法
蜕变的土豆11 小时前
工业监控RTSP取流-实践指南
c++·视频