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: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.
(我们需要标识元素的其他地方是:
boost::thread::null_mutex
到std::scoped_lock
(std::scoped_lock
就像一个关联和交换的二进制操作,消耗较小的锁并产生较大的锁),
std::monostate
到std::variant
(std::variant
就像一个结合和交换的二元运算,消耗较小的联合并产生较大的联合),
- empty 设置为正则表达式中的
operator|
(如果您编写将 NFA 转换为正则表达式的程序,则 operator|
节点的 children 可能会发生),
- 正则表达式中
operator concat
的空字符串,
- ...
)
我们应该如何对待背离?
程序可能:
- 产生一个值;
- 产生错误(程序的状态落在评估函数的范围之外);
- 发散(对于每个状态X,都存在一个状态Y:X ---[评价函数]--> Y);
背离不是数值,背离不是错误,背离就是背离。
存在发散(永不终止)的程序,例如操作系统、协议栈、数据库服务和网络服务器。
有一些方法可以处理分歧。在 C# 中,我们有取消功能和进度报告功能。在 C++ 中,我们可以中断执行代理 (boost.thread) 或销毁执行代理 (boost.context、boost.fiber)。我们可以使用线程安全的队列或通道来 send/receive values/errors to/from 一个连续的演员。
发散用例①:
一位程序员使用 library1 在不可靠的网络上查询一些不可靠的 Web 服务。 library1 永远重试,因为网络不可靠。 library1 本身 不应在某些超时到期时在共享状态中存储异常 因为:
- 应用层程序员可能希望使用不同的取消机制:
- 超时到期时,或
- 当用户单击按钮时,或者
- 用户点击一个按钮开始超时,以及超时到期的时间;
- 应用层程序员可能希望在取消时做不同的事情:
- 提供默认值,或
- 提供例外,或
- 部分程序员不在最上层,不应附加取消机制;
反正程序员一定要用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 = true
或 stared_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
- 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).
- 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)
如您所引用。
当 when_any
通过零期货时,有 4 个设计选项可供选择,不幸的是它们都有意义。
到现在我可以
- 总结每个设计选项的一些弱论点;
- 列出一些实现及其选择的设计选项。
设计选项 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 likeoperator&&
, and in parameter pack expansion the empty pack expands totrue
foroperator&&
,true
is like a future that is immediately ready.
when_any
is likeoperator||
, and in parameter pack expansion the empty pack expands tofalse
foroperator||
,false
is like a future that is never ready.
(我们需要标识元素的其他地方是:
boost::thread::null_mutex
到std::scoped_lock
(std::scoped_lock
就像一个关联和交换的二进制操作,消耗较小的锁并产生较大的锁),std::monostate
到std::variant
(std::variant
就像一个结合和交换的二元运算,消耗较小的联合并产生较大的联合),- empty 设置为正则表达式中的
operator|
(如果您编写将 NFA 转换为正则表达式的程序,则operator|
节点的 children 可能会发生), - 正则表达式中
operator concat
的空字符串, - ...
)
我们应该如何对待背离?
程序可能:
- 产生一个值;
- 产生错误(程序的状态落在评估函数的范围之外);
- 发散(对于每个状态X,都存在一个状态Y:X ---[评价函数]--> Y);
背离不是数值,背离不是错误,背离就是背离。
存在发散(永不终止)的程序,例如操作系统、协议栈、数据库服务和网络服务器。
有一些方法可以处理分歧。在 C# 中,我们有取消功能和进度报告功能。在 C++ 中,我们可以中断执行代理 (boost.thread) 或销毁执行代理 (boost.context、boost.fiber)。我们可以使用线程安全的队列或通道来 send/receive values/errors to/from 一个连续的演员。
发散用例①:
一位程序员使用 library1 在不可靠的网络上查询一些不可靠的 Web 服务。 library1 永远重试,因为网络不可靠。 library1 本身 不应在某些超时到期时在共享状态中存储异常 因为:
- 应用层程序员可能希望使用不同的取消机制:
- 超时到期时,或
- 当用户单击按钮时,或者
- 用户点击一个按钮开始超时,以及超时到期的时间;
- 应用层程序员可能希望在取消时做不同的事情:
- 提供默认值,或
- 提供例外,或
- 部分程序员不在最上层,不应附加取消机制;
反正程序员一定要用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 = true
或 stared_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 internalpromise
is destroyed before any 1 of the 0 provided futures has become ready, and sowhen_any(/*zero args*/)
returns a ready future whoseget()
will throwbroken_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
- 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).
- 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)
如您所引用。