问题描述
当 when_any
传递零期货时,有 4 个设计选项可供选择,不幸的是,所有这些都有意义。
直到现在我能够
- 总结每个设计选项的一些弱论点;
- 列出一些实现以及他们选择的设计选项。
设计选项 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&&
,在参数包扩展中,空包扩展为 true
的 operator&&
,true
就像一个立即出现的未来准备好了。when_any
就像 operator||
,在参数包扩展中,空包扩展到 false
的 operator||
,false
就像一个永远不会准备好的未来。
(我们需要标识元素的其他地方是:
-
boost::thread::null_mutex
到std::scoped_lock
(std::scoped_lock
就像一个关联和交换的二元运算,消耗较小的锁并产生较大的锁), -
std::monostate
到std::variant
(std::variant
就像一个结合和交换的二元运算,消耗较小的联合并产生较大的联合), - 在正则表达式中将空设置为
operator|
(如果您编写将 NFA 转换为正则表达式的程序,则可能会发生具有零个子节点的operator|
节点), - 正则表达式中
operator concat
的空字符串, - ...
)
我们应该如何处理分歧?
一个程序可能:
发散不是值,发散不是错误,发散就是发散。
有些程序会发散(永不终止),例如操作系统、协议栈、数据库服务和 Web 服务器。
有办法处理分歧。在 C# 中,我们有取消功能和进度报告功能。在 C++ 中,我们可以中断执行代理 (boost.thread) 或销毁执行代理 (boost.context,boost.fiber)。我们可以使用线程安全的队列或通道来不断地向/从参与者发送/接收值/错误。
分歧用例①:
程序员使用 library1 在不可靠的网络上查询一些不可靠的 Web 服务。 library1 永远重试,因为网络不可靠。 library1 本身不应在某些超时到期时将异常存储在共享状态,因为:
- 应用层程序员可能想要使用不同的取消机制:
- 应用层程序员可能希望在取消时做不同的事情:
无论如何,程序员必须使用 when_any
将可能永久阻塞的未来与他自己的我的取消/回退机制未来合并,以获得更大的未来,而更大的未来现在不会发散。
(假设 when_any(several future<T>...)
返回 future<T>
,因此我们不必在未来树中的每个中间 when_any 节点上编写样板代码。)
(需要做一些修改:(1)when_any
返回的更大的 Future 应该在第一个子 Future 准备好时销毁其他子 Future;(2)library1 应该使用 promise 对象来检查 if(shared_state.reference_count == 1)
并获得知道消费者已经放弃了未来(即操作被取消),并退出该循环;)
发散用例②:
程序员使用 library2 在不可靠的网络上查询一些不可靠的 Web 服务。 library2 重试 n 次,然后通过在 shared_state(shared_state.diverge = true
或 stared_state.state = state_t::diverge
)中设置一个位来永久阻塞,这不是物理上的,而是逻辑上的。程序员使用 when_any
合并来自 library2 的未来和 my-cancelation/fallback-machanism 未来。第一个准备好的子未来指示结果。假设一个失败的子未来准备好有异常而不是永远阻塞,那么它回答更大的未来,而不是稍后准备好的成功的子未来,这是不希望的。
(假设 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 提供的期货中的任何一个准备就绪之前被销毁,因此 when_any(/*zero args*/)
返回一个准备好的未来其 get()
将抛出 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 绑定程序的函数:
哪里 | 功能 | 传递零任务时的行为 |
---|---|---|
std::experimental *_all | future<sequence<future<T>>> when_all(...) |
返回一个存储空序列的未来 |
std::experimental *_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 合并-如果他缺乏进一步的信息,就可以获得更大的永不阻塞的未来,这很乏味。如果我们不允许这样做,我们将保护那些没有详细记录所有接口的团队(让我打个比方:假设您在一家 C++ 公司,其中库函数接收和返回潜在的 nullptr
指针而不是 {{ 1}} 和 optional<reference_wrapper<T>>
,在没有更多信息或文档的情况下,您必须使用 reference_wrapper<T>
保护每个成员访问表达式,这很乏味;同样,对于 Future,我们必须在任何地方都执行 if(p)
)。所以我们最好让接口和可能性尽可能小。
(向 Alan Birtles 道歉:我很抱歉那天我在列出的实现上犯了一个错误:boost.fiber 的 wait_any 函数除了第一个禁止零期货,还有一个单独的实现会返回一个未来存储broken_promise (https://codereview.stackexchange.com/questions/176168/non-polling-implementation-of-stdwhen-any) 所以我试图在一个新问题中总结这些。)
解决方法
暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!
如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。
小编邮箱:dio#foxmail.com (将#修改为@)