首页 > 解决方案 > wait_for_any/when_any/WaitAny/WhenAny:传递零期货/任务时的正确行为是什么?

问题描述

有 4 种设计选项可供选择何时when_any通过零期货,不幸的是它们都是有意义的。
直到现在我能够

  1. 总结每个设计选项的一些弱论点;
  2. 列出一些实现以及他们选择的设计选项。

设计选项 1:when_any(zero future<T>s)应该返回一个永远阻塞的未来。

第一个原因是定义的简单和统一。

如果 when_any(zero futures) 返回一个永远阻塞的未来,我们得到:

wait_all 在一些未完成时阻塞,直到全部完成;
wait_any 如果一些已经完成则解除阻塞,而不是如果全部未完成;

如果 when_any(zero futures) 返回一个立即准备好的未来,我们得到:

wait_all 在一些未完成时阻塞,直到全部完成;
wait_any 如果一些已完成则解除阻塞,而不是如果所有未完成则解除阻塞,但如果期货为零则解除阻塞;

第二个原因是when_any关联和交换二元运算,因此对于可变参数版本,when_any我们希望返回when_any运算的标识元素(这是一个永远阻塞的未来)。在您可以定义自己的二元运算符(可能是 C++ 将来会这样做)或std::accumulate支持算法的语言中,您迟早仍会遇到此标识元素问题。

when_all就像operator&&,并且在参数包扩展中,空包扩展为truefor operator&&true就像一个立即准备好的未来。
when_any就像operator||,并且在参数包扩展中,空包扩展为falsefor operator||false就像一个永远不会准备好的未来。

(我们需要标识元素的其他地方是:

)

我们应该如何对待分歧?

一个程序可能:

发散不是一个值,发散不是错误,发散就是发散。
有些程序会发散(永不终止),例如操作系统、协议栈、数据库服务和 Web 服务器。
有一些方法可以处理分歧。在 C# 中,我们有取消功能和进度报告功能。在 C++ 中,我们可以中断执行代理 (boost.thread) 或销毁执行代理 (boost.context, boost.fiber)。我们可以使用线程安全的队列或通道来连续地向/从参与者发送/接收值/错误。

分歧用例①:

程序员使用 library1 在不可靠的网络上查询一些不可靠的 Web 服务。library1 永远重试,因为网络不可靠。当某个超时到期时, library1 本身不应在共享状态中存储异常,因为:

  1. 应用层程序员可能想要使用不同的取消机制:
  1. 应用层程序员可能想要在取消时做不同的事情:

无论如何,程序员必须使用when_any将可能永远阻塞的未来与他自己的取消/回退机制未来合并,以获得更大的未来,而更大的未来现在不会分叉。
(假设when_any(several future<T>...)返回future<T>,因此我们不必在未来树中的每个中间 when_any 节点处编写样板代码。)
(需要进行一些修改:(1)返回的更大未来when_any应该在第一个子未来准备好时破坏其他子期货; (2) library1应该使用promise对象来检查if(shared_state.reference_count == 1)并知道消费者已经放弃了future(即操作被取消),并退出那个循环;)

分歧用例②:

程序员使用 library2 在不可靠的网络上查询一些不可靠的 Web 服务。library2 重试 n 次,然后永久阻塞,不是物理上的,而是通过在 shared_state (shared_state.diverge = truestared_state.state = state_t::diverge) 中设置一个位在逻辑上阻塞。程序员when_any用来合并来自 library2 的未来和 my-cancelation/fallback-machanism 未来。第一个准备好的子未来指示结果。假设一个失败的子 future 在异常情况下准备好,而不是永远阻塞,那么它会回答更大的未来,而不是稍后准备好的成功的子 future,这是不希望的。
(假设when_any(several future<T>...)返回future<T>,所以我们不必在未来树中的每个中间 when_any 节点上编写样板代码。)

分歧用例③:

在测试网络代码时,使用永远不会准备好的未来来代表网络状况很差的客户端,使用立即准备好的未来来代表网络状况非常好的客户端,使用具有各种超时的期货来代表对于介于两者之间的客户。
(需要一些修改:(1)添加make_diverging_future;(2)添加make_timeout_ready_future;)

设计选项 2:when_any(zero future<T>s)应该返回一个包含异常的未来。

c++ - std::when_any() 的非轮询实现 - 代码审查堆栈交换 https://codereview.stackexchange.com/questions/176168/non-polling-implementation-of-stdwhen-any

当使用零参数调用时,并发 TS 的when_any哲学错误地返回了一个就绪的未来。我的版本没有特别处理这种情况,因此自然行为会消失:在promise提供的 0 个期货中的任何 1 个已经准备好之前,内部被销毁,因此when_any(/*zero args*/)返回一个准备好的未来,其get()将 throw broken_promise

我认为这是一个“早期失败,大声失败”的案例。由于他将分歧视为错误,因此在上述用例中会出现问题。

设计选项 3:when_any(zero future<T>s)应该返回一个包含 ??? 值的未来。

设计方案 4:when_any(zero future<T>s)应禁止。

标准和库使用最后 3 个设计选项。我将在下面尝试猜测他们的动机。

以下是 *_all 和 *_any 上的一些实现及其行为:
CPU 绑定程序的函数:(如果您在阅读表格时遇到问题,请进入编辑模式

在哪里 功能 传递零任务时的行为
boost.thread *_all void wait_for_all(...) 返回void
boost.thread *_any iterator wait_for_any(...) 返回结束迭代器
boost.fiber *_all void wait_all_simple(...) 在编译时被拒绝
vector<R> wait_all_values(...) 在编译时被拒绝
vector<R> wait_all_until_error(...) 在编译时被拒绝
vector<R> wait_all_collect_errors(...) 在编译时被拒绝
R wait_all_members(...) 返回一个值R
boost.fiber *_any void wait_first_simple(...) 返回void
R wait_first_value(...) 在编译时被拒绝
R wait_first_outcome(...) 在编译时被拒绝
R wait_first_success(...) 在编译时被拒绝
variant<R> wait_first_value_het(...) 在编译时被拒绝
System.Threading.Tasks *_all void Task.WaitAll(...) 返回void
System.Threading.Tasks *_any int Task.WaitAny(...) 返回-1

IO绑定程序的功能:

在哪里 功能 传递零任务时的行为
标准::实验 *_all future<sequence<future<T>>> when_all(...) 返回一个未来存储的空序列
标准::实验 *_any future<...> when_any(...) 返回一个未来的存储{ size index = -1, sequence<future<T>> sequence = empty sequence }
boost.thread *_all future<sequence<future<T>>> when_all(...) 返回一个未来存储的空序列
boost.thread *_any future<sequence<future<T>>> when_any(...) 返回一个未来存储的空序列
System.Threading.Tasks *_all Task<TResult[]> Task.WhenAll(...) 返回一个未来存储的空序列
System.Threading.Tasks *_any Task<Task<TResult>> Task.WhenAny(...) 在运行时被拒绝(抛出ArgumentException

System.Threading.Tasks.WaitAny(...)接受零期货但System.Threading.Tasks.WhenAny(...)在运行时拒绝。)

我们不应该允许when_any(zero tasks)返回一个永远阻塞的未来的原因可能是实用性。如果我们允许这样做,我们会在 future 的接口上打开一个洞,说每个 future 都可能发散,所以每个应用层程序员都必须使用when_any将 future 与 my-cancelation/fallback-machanism 未来合并以获得更大的永不阻塞的未来,如果他缺乏进一步的信息,这很乏味。如果我们不允许这样做,我们将保护那些没有详细记录所有接口的团队(让我打个比方:假设你在一家 C++ 公司,库函数接收并返回潜在nullptr的指针而不是optional<reference_wrapper<T>>and reference_wrapper<T>,没有更多信息或文档你必须保护每个成员访问表达式if(p),这很乏味;与期货类似,我们必须在when_any(future_returned_from_library, std::make_timeout_future(1s))任何地方做)。所以我们最好保持接口和可能性尽可能小。

(向 Alan Birtles 道歉:我很抱歉那天我在列出的实现方面犯了一个错误:boost.fiber 的 wait_any 函数除了第一个函数之外禁止零期货,并且有一个单独的实现返回一个未来存储的broken_promise(https: //codereview.stackexchange.com/questions/176168/non-polling-implementation-of-stdwhen-any)所以我试图在一个新问题中总结这些。)

标签: c++boostfuture

解决方案


我会选择标准解决方案:https ://en.cppreference.com/w/cpp/experimental/when_any

  1. 如果范围为空(即 first == last),则返回的 future 立即准备好;when_any_result 的 futures 字段为空向量,index 字段为 size_t(-1)。
  2. 如果没有提供参数,则返回的 future 立即准备好;when_any_result 的 futures 字段为空元组,index 字段为 size_t(-1)。

还要注意,大多数其他“潜在的可取行为”可能很容易通过在任何接收到的列表中添加一个“空”种子未来来组合:

 auto my_when_any = [](auto... f) {
     return when_any(EmptyT{}, f...);
 };

EmptyT可能是一个随时准备好的未来,永远不会准备好,或者根据您的喜好持有例外。

这与您决定幺半群“种子”的折叠表达式非常相似:(false || ... || pack)(true && ... && pack)您所引用的相比。


推荐阅读