【问题标题】:C++20 Coroutines, Unexpected reordering of await_resume, return_value and yield_valueC++20 协程,await_resume、return_value 和 yield_value 的意外重新排序
【发布时间】:2021-01-28 13:17:25
【问题描述】:

背景

我有一个既可以co_return 也可以co_yield 的任务类型。 在 LLVM 中,该任务按预期工作并通过了一些早期测试。在 MSVC 和 GCC 中,代码以相同的方式失败(巧合?)。


小问题

具有以下测试功能:

Task<int> test_yielding()
{
    co_yield 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_resumereturn_valueyield_value


详情

我已经通过 clang tidy、PVS 工作室运行了代码,启用了 LLVM、GCC、MSVC 中的所有可用消毒剂,并且没有任何相关弹出(只是关于销毁和恢复的 cmets 不是 noexcept)。

我有几个非常相似的测试: 相关测试是:

功能:

Task<int> test_yielding()
{
    co_yield 1;
    co_return 2;
}

测试 1(通过):

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);

测试 2(失败):

Title("Test co_yield + co_return rvalue");
auto fn = test_yielding();
auto res =
(
    co_await fn +
    co_await fn
);
ASSERT(res == 3);

测试 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

测试 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

如果您查看有效的 MSVC FAIL 和 MSVC PASS 之间的差异(地址已更正,则会出现以下内容): 这清楚地表明以下行已重新排序:

        AwaitAwaitable: await_resume: 02901E20  
        GetValue: 02901E20

LLVM 和 GCC 的来源和结果是 here

查看 GCC FAIL 和 LLVM PASS 之间的测试 2 差异: GCC 中也发生了非常相似的重新排序。

差异中突出显示的行产生于以下来源:

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();
    }
};

有人知道这里发生了什么吗,这是编译器/库/用户错误吗?

【问题讨论】:

  • 允许编译器在检索值并添加它们之前评估co_await fn。由于两者都将结果存储在相同的Task&lt;int&gt; 中,因此它会被最后一个值(在本例中为 2)覆盖。而auto a = co_await fn 复制第一个co_await 之后的结果。我认为这就是正在发生的事情。 AFAIK (co_await fn + co_await fn) 行的结果定义不明确。
  • 它们可以交错,即 [eval foo()] -> [eval bar()] -> [read foo() result] -> [read bar() result] 是可能的。至少我是这样理解en.cppreference.com/w/cpp/language/eval_order“排序”部分:“它们可以按任何顺序执行并且可能重叠”
  • 嗯,虽然规则 11 与此相矛盾。至少对于正常的函数调用。但是我在关于订购的标准中找不到任何关于co_await 的信息。我想现在是 UB。
  • 那是不真实的,这么简单的表达。这需要修复:|
  • 这是核心问题 2466(尚未在公开问题列表中)。

标签: c++ c++20 c++-coroutine


【解决方案1】:

观察到的行为似乎是由于 GCC 和 MSVC 在处理加法运算符时存在类似错误,其中参数都是 co_await 表达式。

在这种情况下,GCC 和 MSVC 在从第二个挂起点恢复之后(即在执行加法之前)似乎都错误地对两个 co_await 表达式的 await_resume() 调用排序。

他们应该在从第一个挂起点恢复之后和开始计算第二个 co_await 表达式之前立即对第一个 co_await 表达式(不确定是哪一个)的调用进行排序。

【讨论】:

  • 感谢刘易斯,感谢 cppcoro!我一直试图引起这个问题的注意。我的 GCC 报告在这里:gcc.gnu.org/bugzilla/show_bug.cgi?id=97433
  • 标准在哪里说这是不正确的?
  • 只是用更好的链接替换我的评论 :) eel.is/c++draft/intro.execution#9
  • Section [expr.await] p5.2 声明: > 如果 await-ready 的结果为真,或者当协程恢复时,将评估 await-resume 表达式,其结果是等待表达式的结果。这对我来说意味着当协程恢复时会立即评估等待表达式。但是如果我们阅读它是关于 [intro.execution] p10 的,它表示除非另有说明,否则子表达式是未排序的,而 [expr.add] 没有为内置 operator+ 参数指定任何排序,那么编译器可能会将 co_await 部分视为未排序的...?
  • 是的,我认为这应该是一个缺陷 - 如果对规范的解释是正确的。
猜你喜欢
  • 2021-07-08
  • 2021-07-22
  • 1970-01-01
  • 2021-01-14
  • 2017-09-16
  • 2020-01-22
  • 2017-10-07
  • 2023-03-28
  • 2020-02-26
相关资源
最近更新 更多