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

Coroutine frame overridden by other coroutine? (GCC 11.3 on -O2 and higher)

提问人:Lukas Lang 提问时间:10/27/2023 最后编辑:Lukas Lang 更新时间:10/29/2023 访问量:41

问:

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

代码如下:(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();
}

代码说明

代码首先定义了 ,一个基本的协程类型。然后,它创建一个包含两个 (print 和 、 respecitely) 的数组,并恢复它们的协程句柄。tasktaskab

结果

Godbolt 链接配置了三个编译器:

  • GCC 11.3, : 正确打印-O1ab
  • GCC 11.3, : 打印-O2bb
  • GCC 12.1:正确打印-O3ab

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

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

C++ GCC 协程 编译器-bug

评论


答:

0赞 Lukas Lang 10/29/2023 #1

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

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

  • 确保 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 仅在协程被销毁后才会被销毁。(假设协程在函数退出之前完成)