C ++ 20协程,await_resume,return_value和yield_value的意外重新排序 [英] C++20 Coroutines, Unexpected reordering of await_resume, return_value and yield_value

查看:110
本文介绍了C ++ 20协程,await_resume,return_value和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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆