c++ - wait_for_any/when_any/WaitAny/WhenAny:传递零期货/任务时的正确行为是什么?
问题描述
有 4 种设计选项可供选择何时when_any
通过零期货,不幸的是它们都是有意义的。
直到现在我能够
- 总结每个设计选项的一些弱论点;
- 列出一些实现以及他们选择的设计选项。
设计选项 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
foroperator&&
,true
就像一个立即准备好的未来。
when_any
就像operator||
,并且在参数包扩展中,空包扩展为false
foroperator||
,false
就像一个永远不会准备好的未来。
(我们需要标识元素的其他地方是:
boost::thread::null_mutex
tostd::scoped_lock
(std::scoped_lock
就像一个关联和交换二元运算,消耗更小的锁并产生更大的锁),std::monostate
tostd::variant
(std::variant
就像一个关联和交换二元运算,消耗较小的联合并产生较大的联合),operator|
在正则表达式中设置为空(operator|
如果您编写将 NFA 转换为正则表达式的程序,则可能会发生具有零个子节点的节点),operator concat
正则表达式中的空字符串,- ...
)
我们应该如何对待分歧?
一个程序可能:
- 产生价值;
- 产生错误(程序的状态超出评估函数的域);
- 发散(对于每个状态 X,都存在一个状态 Y:X ---[evaluation function]--> Y);
发散不是一个值,发散不是错误,发散就是发散。
有些程序会发散(永不终止),例如操作系统、协议栈、数据库服务和 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
应该在第一个子未来准备好时破坏其他子期货; (2) library1应该使用promise对象来检查if(shared_state.reference_count == 1)
并知道消费者已经放弃了future(即操作被取消),并退出那个循环;)
分歧用例②:
程序员使用 library2 在不可靠的网络上查询一些不可靠的 Web 服务。library2 重试 n 次,然后永久阻塞,不是物理上的,而是通过在 shared_state (shared_state.diverge = true
或stared_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()
将 throwbroken_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)所以我试图在一个新问题中总结这些。)
解决方案
我会选择标准解决方案:https ://en.cppreference.com/w/cpp/experimental/when_any
- 如果范围为空(即 first == last),则返回的 future 立即准备好;when_any_result 的 futures 字段为空向量,index 字段为 size_t(-1)。
- 如果没有提供参数,则返回的 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)
您所引用的相比。
推荐阅读
- android - {MAC OS BIG SUR} 从 expo 应用程序扫描二维码时出现“网络响应超时”错误
- npm - 无法在 Ubuntu 18.04 中运行 npm start 或 npm install
- java - “BasicBatchConfigurer”已保护访问 - Spring Batch/Spring Boot 未将数据持久化到数据库
- proxy - 在 python 中构建代理服务器
- python - 删除带有条件的 RDD 值
- swagger-ui - quarkus-smallrye-openapi - 无法解析引用:无法解析指针:/components/schemas
- c++ - MPI Scatter 从最终分区中丢失值
- python - 我是一名python学生,我有一个问题
- c# - Azure 应用服务在 GitHub 上构建和部署失败
- postgresql - 如何在 PostgreSQL 中执行一对多关系,其中子行和父行之间没有精确的外键匹配?