协程框架被其他协程覆盖? (-O2 及更高版本上的 GCC 11.3)

问题描述 投票:0回答:1

我在 GCC 11.3 中的协程中遇到了一个问题:我实现了一个事件循环,其中多个协程交替前进(如果它们的等待再次准备好)。我最近注意到优化后的构建没有按预期运行,我相信我已经将其范围缩小到被其他协程以某种方式覆盖的框架。

这是代码:(Godbolt 链接

#include <array>
#include <cstdio>
#include <coroutine>

struct task
{
    struct promise_type
    {
        auto get_return_object() -> task
        {
            return task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        auto initial_suspend() noexcept -> std::suspend_always  { return {}; }
        auto final_suspend() noexcept -> std::suspend_always  { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };

    task(std::coroutine_handle<promise_type> handle_) : handle{handle_} {}

    std::coroutine_handle<promise_type> handle;
};

auto main(void) -> int
{
    auto a = "a";
    auto b = "b";

    std::array tasks = {
        [&]() -> task { 
            printf(a);
            co_return;
        }(),
        [&]() -> task
        {
            printf(b);
            co_return;
        }()
    };

    for (auto& task : tasks)
        task.handle.resume();
}

代码解释

代码首先定义了

task
,一个基本的协程类型。然后,它创建两个
task
的数组(分别打印
a
b
),并恢复它们的协程句柄。

结果

Godbolt link 配置了三个编译器:

  • GCC 11.3,
    -O1
    :正确打印
    ab
  • GCC 11.3,
    -O2
    :打印
    bb
  • GCC 12.1,
    -O3
    :正确打印
    ab

由于 12.1 不再显示该问题,看起来有些问题已得到修复。不幸的是,我无法更新我的编译器,因此我试图了解触发问题的原因以及如何避免它。所以:

发生了什么事?这是编译器错误吗?如何解决这个问题?

c++ gcc coroutine compiler-bug
1个回答
0
投票

进一步减少我的 MWE 并随后重新表述我的搜索词后,我能够找到问题的答案:该错误存在于 C++ 标准中,而不是 GCC

发生的事情是,

task
的 lambda 是通过引用/指针在协程框架中捕获的(据我所知,按照标准规定)。由于协程对象是通过立即调用的 lambda 表达式创建的,因此一旦协程最初挂起(在我的情况下立即挂起),lambda 对象本身就会超出范围。要解决这个问题,我们有两个选择:

  • 确保 lambda 在协程中幸存下来。
  • 确保在协程首次挂起之前将 lambda 捕获复制到局部变量中。

第一个选项可以按如下方式实现(Godbolt):

#include <array>
#include <cstdio>
#include <coroutine>

struct task
{
    struct promise_type
    {
        auto get_return_object() -> task
        {
            return task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        auto initial_suspend() noexcept -> std::suspend_always  { return {}; }
        auto final_suspend() noexcept -> std::suspend_always  { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };

    task(std::coroutine_handle<promise_type> handle_) : handle{handle_} {}

    std::coroutine_handle<promise_type> handle;
};

auto main(void) -> int
{
    auto a = "a";
    auto b = "b";

    auto task_a = [&]() -> task { 
            printf(a);
            co_return;
        };
    auto task_b = [&]() -> task
        {
            printf(b);
            co_return;
        };
    std::array tasks = {
        task_a(),
        task_b()
    };

    for (auto& task : tasks)
        task.handle.resume();
}

请注意,在根据 lambda 表达式构造协程之前,我是如何将它们本身简单地存储在局部变量中的。这保证了 lambda 仅在协程被销毁后才被销毁。 (假设协程在函数退出之前完成)

© www.soinside.com 2019 - 2024. All rights reserved.