wait_for_any/when_any/WaitAny/WhenAny:传递零 futures/tasks 时的正确行为是什么?

wait_for_any/when_any/WaitAny/WhenAny: What is the correct behavior when passed zero futures/tasks?

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

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

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

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

如果when_any(零期货)return一个永远阻塞的未来,我们得到:

wait_all blocks while some is unfinished, until all is finished;
wait_any unblocks if some is finished, not if all is unfinished;

如果when_any(零期货)return一个立即准备好的未来我们得到:

wait_all blocks while some is unfinished, until all is finished;
wait_any unblocks if some is finished, not if all is unfinished, but unblocks if there are zero futures;

第二个原因是 when_any 是结合和交换二元运算,所以对于 when_any 的可变版本,我们想要 return when_any 操作(这是一个永远阻塞的未来)。而在你可以定义自己的二元运算符的语言中(可能 C++ 将来会这样做)或者支持 std::accumulate 算法的语言,你迟早还是会遇到这个单位元素问题。

when_all is like operator&&, and in parameter pack expansion the empty pack expands to true for operator&&, true is like a future that is immediately ready.
when_any is like operator||, and in parameter pack expansion the empty pack expands to false for operator||, false is like a future that is never ready.

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

)

我们应该如何对待背离?

程序可能:

背离不是数值,背离不是错误,背离就是背离。
存在发散(永不终止)的程序,例如操作系统、协议栈、数据库服务和网络服务器。
有一些方法可以处理分歧。在 C# 中,我们有取消功能和进度报告功能。在 C++ 中,我们可以中断执行代理 (boost.thread) 或销毁执行代理 (boost.context、boost.fiber)。我们可以使用线程安全的队列或通道来 send/receive values/errors to/from 一个连续的演员。

发散用例①:

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

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

反正程序员一定要用when_any把potentially-block-forever的未来和自己的my-cancelation/fallback-machanism的未来融合在一起,才能得到更大的未来,更大的未来不会现在分叉。
(假设 when_any(several future<T>...) returns future<T> 所以我们不必在未来树中的每个中间 when_any 节点编写样板代码。)
(需要进行一些修改:(1)当第一个 child 未来准备就绪时,when_any returns 应该销毁其他 child 未来的更大未来;(2)library1 应该使用promise object 检查 if(shared_state.reference_count == 1) 并知道消费者已经放弃了未来(即操作被取消),并退出该循环;)

发散用例②:

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

发散用例③:

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

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

c++ - Non-polling std::when_any() 的实现 https://codereview.stackexchange.com/questions/176168/non-polling-implementation-of-stdwhen-any

The Concurrency TS's when_any philosophically-incorrectly returns a ready future when called with zero arguments. My version doesn't treat that case specially, and so the natural behavior falls out: the internal promise is destroyed before any 1 of the 0 provided futures has become ready, and so when_any(/*zero args*/) returns a ready future whose get() will throw broken_promise.

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

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

设计选项 4:when_any(zero future<T>s) 应该被禁止。

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

下面是一些实现及其在 *_all 和 *_any 上的行为:
CPU-bound 程序的功能:(如果您在阅读 table 时遇到问题,请转到编辑模式

where function behavior when passed zero tasks
boost.thread *_all void wait_for_all(...) return void
boost.thread *_any iterator wait_for_any(...) return the end iterator
boost.fiber *_all void wait_all_simple(...) refused at compile time
vector<R> wait_all_values(...) refused at compile time
vector<R> wait_all_until_error(...) refused at compile time
vector<R> wait_all_collect_errors(...) refused at compile time
R wait_all_members(...) return a value of R
boost.fiber *_any void wait_first_simple(...) return void
R wait_first_value(...) refused at compile time
R wait_first_outcome(...) refused at compile time
R wait_first_success(...) refused at compile time
variant<R> wait_first_value_het(...) refused at compile time
System.Threading.Tasks *_all void Task.WaitAll(...) return void
System.Threading.Tasks *_any int Task.WaitAny(...) return -1

IO-bound 个程序的函数:

where function behavior when passed zero tasks
std::experimental *_all future<sequence<future<T>>> when_all(...) return a future storing empty sequence
std::experimental *_any future<...> when_any(...) return a future storing { size index = -1, sequence<future<T>> sequence = empty sequence }
boost.thread *_all future<sequence<future<T>>> when_all(...) return a future storing empty sequence
boost.thread *_any future<sequence<future<T>>> when_any(...) return a future storing empty sequence
System.Threading.Tasks *_all Task<TResult[]> Task.WhenAll(...) return a future storing empty sequence
System.Threading.Tasks *_any Task<Task<TResult>> Task.WhenAny(...) refused at run time (throw ArgumentException)

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

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

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

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

  1. If the range is empty (i.e., first == last), the returned future is ready immediately; the futures field of the when_any_result is an empty vector, and the index field is size_t(-1).
  2. If no argument is provided, the returned future is ready immediately; the futures field of the when_any_result is an empty tuple, and the index field is size_t(-1).

另请注意,大多数其他“潜在的理想行为”可能只需在任何收到的列表中添加一个“空”种子期货即可轻松构成:

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

此处 EmptyT 可能是一个随时准备就绪、永远不会准备好或有例外的未来,具体取决于您的偏好。

这与例如折叠 you 决定幺半群“种子”的表达式:(false || ... || pack)(true && ... && pack) 如您所引用。