C ++ 20协程,await_resume,return_value和yield_value的意外重新排序 [英] C++20 Coroutines, Unexpected reordering of await_resume, return_value and yield_value
问题描述
背景
我的任务类型可以同时 co_return
和 co_yield
.在LLVM中,任务按预期方式工作并通过了一些早期测试.在MSVC和GCC中,代码以相同的方式失败(巧合吗?).
简要问题
具有以下测试功能:
Task< int>test_yielding(){共产1;co_return 2;}
从Task对象检索到两个值.
auto a = co_await fn;auto b = co_await fn;
a的值应为1,b的值应为2.
针对 a + b == 3
测试结果.
以上测试通过,但是以下测试失败:
auto res = co_await fn + co_await fn
GCC和MSVC的res值为4.两者均从最终的co_return中检索.据我了解, co_await fn
的第一个和第二个调用应该分别为1和2.
在MSVC和GCC中,代码失败,因为它们似乎重新排序了 await_resume
, return_value
和 yield_value
.
详细信息
我已经通过c ++整洁的PVS studio运行了代码,启用了LLVM,GCC,MSVC中的所有可用的消毒器,并且没有弹出任何相关消息(只是有关销毁和恢复的注释,并非例外).
我有几个非常相似的测试:相关测试是:
功能:
Task< int>test_yielding(){共产1;co_return 2;}
测试1(通过):
Title(测试co_yield + co_return左值");自动fn = test_yielding();自动a = co_await fn;auto b = co_await fn;ASSERT(a + b == 3);
测试2(失败):
Title("Test co_yield + co_return rvalue");自动fn = test_yielding();自动解析=(co_await fn +co_await fn);ASSERT(res == 3);
测试MSVC 1(PASS)的结果:
---------------------------------标题测试co_yield + co_return左值---------------------------------get_return_object:02F01DA0initial_suspend:02F01DA0await_transform:02D03C80等待中等待:await_ready:02F01DA0等待等待:await_suspend:02F01DA0设定电流:02F01DA0继续:02F01DA0yield_value:02F01DA0设定值:02F01DA0YieldAwaitable:await_ready:02F01DA0YieldAwaitable:await_suspend:02F01DA0继续:02F01DA0等待中等待中:await_resume:02F01DA0获取值:02F01DA0await_transform:02D03C80等待中等待:await_ready:02F01DA0Await等待的:await_suspend:02F01DA0设定电流:02F01DA0继续:02F01DA0YieldAwaitable:await_resume:02F01DA0返回值:02F01DA0设定值:02F01DA0最终暂停:02F01DA0YieldAwaitable:await_ready:02F01DA0YieldAwaitable:await_suspend:02F01DA0继续:02F01DA0等待中等待中:await_resume:02F01DA0获取值:02F01DA0通过test_task:323 a + b == 3[结果= 3,预期= 3]销毁:02F01DA0
测试MSVC 2(失败)的结果:
---------------------------------标题测试co_yield + co_return右值---------------------------------get_return_object:02F01CA0initial_suspend:02F01CA0await_transform:02D03C80Await等待的:await_ready:02F01CA0Await等待的:await_suspend:02F01CA0设定电流:02F01CA0继续:02F01CA0yield_value:02F01CA0设定值:02F01CA0YieldAwaitable:await_ready:02F01CA0YieldAwaitable:await_suspend:02F01CA0继续:02F01CA0await_transform:02D03C80Await等待的:await_ready:02F01CA0Await等待的:await_suspend:02F01CA0设定电流:02F01CA0继续:02F01CA0YieldAwaitable:await_resume:02F01CA0返回值:02F01CA0设定值:02F01CA0最终暂停:02F01CA0YieldAwaitable:await_ready:02F01CA0YieldAwaitable:await_suspend:02F01CA0继续:02F01CA0等待中等待中:await_resume:02F01CA0获取值:02F01CA0等待中等待中:await_resume:02F01CA0获取值:02F01CA0失败test_task:342 res == 3[结果= 4,预期= 3]销毁:02F01CA0
如果您查看正常工作的MSVC FAIL和MSVC PASS之间的区别(已更正地址,将显示以下内容):很明显,以下几行已重新排序:
Await可等待:await_resume:02901E20获取值:02901E20
LLVM和GCC的来源和结果在在海湾合作委员会中发生了非常类似的事情.
差异中突出显示的行是由以下来源产生的:
模板< typename Promise>struct AwaitAwaitable{承诺与承诺m_promise;布尔await_ready()const noexcept{WriteLine("AwaitAwaitable:",__ func __,:",& m_promise);返回false;}无效await_suspend(default_handle句柄)noexcept{WriteLine("AwaitAwaitable:",__ func __,:",& m_promise);m_promise.SetCurrent(m_promise.Handle());m_promise.ContinueWith(handle);}自动await_resume()const noexcept{WriteLine("AwaitAwaitable:",__ func __,:",& m_promise);返回m_promise.GetValue();}};
有人知道这是怎么回事,这是编译器/库/用户错误吗?
观察到的行为似乎是由于GCC和MSVC在处理加法运算符时都存在类似的错误所致,其中两个参数均为 co_await
表达式.
在这种情况下,从第二个暂挂点恢复后,GCC和MSVC似乎都错误地对两个 co_await
表达式的 await_resume()
的调用排序.就在执行加法之前.)
相反,他们应该在从第一个挂起点恢复后立即对第一个 co_await
表达式(不确定哪个)的调用进行排序(对第一个[code_await_resume()不确定).在开始计算第二个 co_await
表达式之前.
Background
I have a task type that can both co_return
and co_yield
.
In LLVM the task works as expected and passes some early tests. In MSVC and GCC the code fails in the same manner (coincidence?).
Brief Problem
With the following test function:
Task<int> test_yielding()
{
co_yield 1;
co_return 2;
}
There are two values retrieved from a Task object.
auto a = co_await fn;
auto b = co_await fn;
The value of a is expected to be 1, the value of b is expected to be 2.
The result is tested against a + b == 3
.
The above test passes, however the following test fails:
auto res = co_await fn + co_await fn
The value of res for GCC and MSVC is 4. Both retrieved from the final co_return. As I understand it the first and second calls of co_await fn
should be 1 and 2 in either order.
In MSVC and GCC the code fails as they seems to reorder await_resume
, return_value
and yield_value
.
Details
I have run the code through clang tidy, PVS studio, enabled all the available sanitisers in LLVM, GCC, MSVC and nothing relevant pops up (just comments about destroy and resume not being noexcept).
I have several very similar tests: The relevant tests are:
Function:
Task<int> test_yielding()
{
co_yield 1;
co_return 2;
}
Test 1 (PASS):
Title("Test co_yield + co_return lvalue");
auto fn = test_yielding();
auto a = co_await fn;
auto b = co_await fn;
ASSERT(a + b == 3);
Test 2 (FAIL):
Title("Test co_yield + co_return rvalue");
auto fn = test_yielding();
auto res =
(
co_await fn +
co_await fn
);
ASSERT(res == 3);
The result of the test MSVC 1 (PASS):
---------------------------------
Title Test co_yield + co_return lvalue
---------------------------------
get_return_object: 02F01DA0
initial_suspend: 02F01DA0
await_transform: 02D03C80
AwaitAwaitable: await_ready: 02F01DA0
AwaitAwaitable: await_suspend: 02F01DA0
SetCurrent: 02F01DA0
ContinueWith: 02F01DA0
yield_value: 02F01DA0
SetValue: 02F01DA0
YieldAwaitable: await_ready: 02F01DA0
YieldAwaitable: await_suspend: 02F01DA0
ContinueWith: 02F01DA0
AwaitAwaitable: await_resume: 02F01DA0
GetValue: 02F01DA0
await_transform: 02D03C80
AwaitAwaitable: await_ready: 02F01DA0
AwaitAwaitable: await_suspend: 02F01DA0
SetCurrent: 02F01DA0
ContinueWith: 02F01DA0
YieldAwaitable: await_resume: 02F01DA0
return_value: 02F01DA0
SetValue: 02F01DA0
final_suspend: 02F01DA0
YieldAwaitable: await_ready: 02F01DA0
YieldAwaitable: await_suspend: 02F01DA0
ContinueWith: 02F01DA0
AwaitAwaitable: await_resume: 02F01DA0
GetValue: 02F01DA0
PASS test_task:323 a + b == 3
[ result = 3, expected = 3 ]
Destroy: 02F01DA0
The result of the test MSVC 2 (FAIL):
---------------------------------
Title Test co_yield + co_return rvalue
---------------------------------
get_return_object: 02F01CA0
initial_suspend: 02F01CA0
await_transform: 02D03C80
AwaitAwaitable: await_ready: 02F01CA0
AwaitAwaitable: await_suspend: 02F01CA0
SetCurrent: 02F01CA0
ContinueWith: 02F01CA0
yield_value: 02F01CA0
SetValue: 02F01CA0
YieldAwaitable: await_ready: 02F01CA0
YieldAwaitable: await_suspend: 02F01CA0
ContinueWith: 02F01CA0
await_transform: 02D03C80
AwaitAwaitable: await_ready: 02F01CA0
AwaitAwaitable: await_suspend: 02F01CA0
SetCurrent: 02F01CA0
ContinueWith: 02F01CA0
YieldAwaitable: await_resume: 02F01CA0
return_value: 02F01CA0
SetValue: 02F01CA0
final_suspend: 02F01CA0
YieldAwaitable: await_ready: 02F01CA0
YieldAwaitable: await_suspend: 02F01CA0
ContinueWith: 02F01CA0
AwaitAwaitable: await_resume: 02F01CA0
GetValue: 02F01CA0
AwaitAwaitable: await_resume: 02F01CA0
GetValue: 02F01CA0
FAIL test_task:342 res == 3
[ result = 4, expected = 3 ]
Destroy: 02F01CA0
If you look at the diff between working MSVC FAIL and MSVC PASS (with addresses corrected, the following appears): Which makes it clear that the following lines were reordered:
AwaitAwaitable: await_resume: 02901E20
GetValue: 02901E20
The source and results for LLVM and GCC are here.
Looking at the test 2 diff between the GCC FAIL and LLVM PASS: A very similar reording is occuring in GCC.
The highlighted lines in the diff are produced be the following source:
template <typename Promise>
struct AwaitAwaitable
{
Promise & m_promise;
bool await_ready() const noexcept
{
WriteLine("AwaitAwaitable: ", __func__, ": ", &m_promise);
return false;
}
void await_suspend(default_handle handle) noexcept
{
WriteLine("AwaitAwaitable: ", __func__, ": ", &m_promise);
m_promise.SetCurrent( m_promise.Handle() );
m_promise.ContinueWith( handle );
}
auto await_resume() const noexcept
{
WriteLine("AwaitAwaitable: ", __func__, ": ", &m_promise);
return m_promise.GetValue();
}
};
Does anybody know what is going on here, is this a compiler/library/user error?
The observed behaviour appears to be due to similar bugs in both GCC and MSVC in their handling of the addition-operator where the arguments are both co_await
expressions.
In this instance, both GCC and MSVC seem to be incorrectly sequencing the call to await_resume()
for both co_await
expressions after resumption from the second suspend-point (ie. just before the addition is performed).
They should instead be sequencing the call to await_resume()
for the first co_await
expression (it is indeterminate which one) immediately after resuming from the first suspend-point and before beginning to evaluate the second co_await
expression.
这篇关于C ++ 20协程,await_resume,return_value和yield_value的意外重新排序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!