英文:
`strand::running_in_this_thread()` false positive
问题
我有以下的代码示例
(假设包含了asio头文件和使用指令)
io_context _io_ctx;
auto _strand1 = make_strand(_io_ctx);
auto _strand2 = make_strand(_io_ctx);
asio::awaitable<void> g()
{
auto ex = co_await asio::this_coro::executor;
assert(_strand1.running_in_this_thread()); // *** This assertion should fail, but doesn't
assert(_strand2.running_in_this_thread());
// the following line can be replaced with `sleep(1)` to the same effect
co_await steady_timer{ex, std::chrono::milliseconds{10}}.async_wait(use_awaitable);
assert(!_strand1.running_in_this_thread()); // Here it works as expected
assert(_strand2.running_in_this_thread());
co_return;
}
asio::awaitable<void> f()
{
auto ex = co_await asio::this_coro::executor;
for (;;)
{
assert(_strand1.running_in_this_thread());
assert(!_strand2.running_in_this_thread());
co_await steady_timer{ex, std::chrono::milliseconds{5}}.async_wait(use_awaitable);
assert(_strand1.running_in_this_thread());
assert(!_strand2.running_in_this_thread());
co_spawn(_strand2, g, detached);
}
}
int main()
{
co_spawn(_strand1, f, detached);
_io_ctx.run();
}
这个程序是单线程的(我知道使用strand没有意义,但它们应该按预期工作)。问题是:为什么在协程g
中,两个strand的running_in_this_thread()
都返回true
呢?这种行为对通过strands调度函数有什么影响?
我猜测strand不急于更改它的内部状态以报告"不在运行"(它给出了错误的正面回应)。
编辑
在多个线程上运行上下文使第一次在g
上调用_strand1.running_in_this_thread()
的结果不可预测。我认为这取决于g
是否在同一线程上紧随f
之后运行或不运行。
英文:
I have the following code sample
(assume asio headers included and using directives present)
io_context _io_ctx;
auto _strand1 = make_strand(_io_ctx);
auto _strand2 = make_strand(_io_ctx);
asio::awaitable<void> g()
{
auto ex = co_await asio::this_coro::executor;
assert(_strand1.running_in_this_thread()); // *** This assertion should fail, but doesn't
assert(_strand2.running_in_this_thread());
// the following line can be replaced with `sleep(1)` to the same effect
co_await steady_timer{ex, std::chrono::milliseconds{10}}.async_wait(use_awaitable);
assert(!_strand1.running_in_this_thread()); // Here it works as expected
assert(_strand2.running_in_this_thread());
co_return;
}
asio::awaitable<void> f()
{
auto ex = co_await asio::this_coro::executor;
for (;;)
{
assert(_strand1.running_in_this_thread());
assert(!_strand2.running_in_this_thread());
co_await steady_timer{ex, std::chrono::milliseconds{5}}.async_wait(use_awaitable);
assert(_strand1.running_in_this_thread());
assert(!_strand2.running_in_this_thread());
co_spawn(_strand2, g, detached);
}
}
int main()
{
co_spawn(_strand1, f, detached);
_io_ctx.run();
}
The program is single-thread (I know there's no point using strands, but they should do their work anyway). The question is: why are both strands returning true
to running_in_this_thread()
in coroutine g
? And what implications does this behavior have to the scheduling of functions through the strands?
My guess is that the strand is not eager to change it's internal state as to report "not running" (it gives a false positive).
EDIT
Running the context on multiple threads makes the result of the first call to _strand1.running_in_this_thread()
on g
unpredictable. I presume it depends on whether g
is run right after f
on the same thread or not.
答案1
得分: 4
你的假设与现实不符。
事实上,ASIO并不提供你期望的保证。实际上,文档甚至特意声明了反面情况:
该实现不保证通过不同的strand对象发布或调度的处理程序将同时被调用。
这里有许多因素在起作用。
首先,strand不需要是唯一/不同的,完全停止。事实上,一个天真的实现,其中每个strand都会获得自己独特的锁实例,对性能来说是不利的,可能会导致不必要的高资源使用率。
相反,所有strand服务都使用哈希映射逻辑strand到“真实strand”。桶的数量是可配置的,请参见BOOST_ASIO_HASH_MAP_BUCKETS
确定Boost.Asio内部hash_map对象中的桶数。该值应该是升序的质数的逗号分隔列表。随着地图中元素的数量增加,哈希映射实现将自动增加桶的数量。
一些示例:
- 将
BOOST_ASIO_HASH_MAP_BUCKETS
定义为1021
意味着hash_map对象始终包含1021个桶,不管地图中的元素数量如何。- 将
BOOST_ASIO_HASH_MAP_BUCKETS
定义为53,389,1543
意味着hash_map对象最初包含53个桶。随着元素的添加,桶的数量将增加到389,然后增加到1543。
最后,正如你已经发现的(在你的编辑中),实现只需要维护所做的保证。它不需要损害行为来保证相反的情况。特别是,完成可能被调度而不是立即发布(但永远不会立即调用)。Asio的聪明线程本地队列优化有助于防止常见情况下不必要的开销。
英文:
Your assumptions don't match reality.
Indeed, ASIO does not provide the guarantees you're expecting. In fact, the documentation goes out of their way to even state the corollary:
> The implementation makes no guarantee that handlers posted or dispatched through different strand objects will be invoked concurrently.
A number of factors are at play here.
First, strands don't need to be unique/distinct, full stop. In fact, a naive implementation where each strand would get it's own unique lock instance would be bad for performance and potentially have unnecessarily high resource usage.
Instead, all strand services employ hashing to map logical strands onto "real strands". The number of buckets is configurable, see BOOST_ASIO_HASH_MAP_BUCKETS
> Determines the number of buckets in Boost.Asio's internal hash_map
> objects. The value should be a comma separated list of prime numbers,
> in ascending order. The hash_map implementation will automatically
> increase the number of buckets as the number of elements in the map
> increases.
>
> Some examples:
>
> - Defining BOOST_ASIO_HASH_MAP_BUCKETS
to 1021
means that the hash_map objects will always contain 1021 buckets, irrespective of the
> number of elements in the map.
> - Defining BOOST_ASIO_HASH_MAP_BUCKETS
to 53,389,1543
means that the hash_map objects will initially contain 53 buckets. The number of
> buckets will be increased to 389 and then 1543 as elements are added
> to the map.
Lastly, as you have found out (in your edit), the implementation only need to uphold the guarantees made¹. It does not need to pessimize behaviour to guarantee the opposite(s). In particular, completions may be dispatched instead of posted (though never immediately invoked). Asio's clever thread-local queue optimizations help prevent unnecessary overhead for common situations.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论