我在 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 配置了三个编译器:
-O1
:正确打印ab
-O2
:打印bb
-O3
:正确打印ab
由于 12.1 不再显示该问题,看起来有些问题已得到修复。不幸的是,我无法更新我的编译器,因此我试图了解触发问题的原因以及如何避免它。所以:
发生了什么事?这是编译器错误吗?如何解决这个问题?
进一步减少我的 MWE 并随后重新表述我的搜索词后,我能够找到问题的答案:该错误存在于 C++ 标准中,而不是 GCC。
发生的事情是,
task
的 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 仅在协程被销毁后才被销毁。 (假设协程在函数退出之前完成)