无同步时跨线程co_await并恢复C++协程是否安全?
cppreference提供了一个co_await示例,该示例会在另一个线程恢复协程。但在不进行同步的情况下,跨线程co_await并恢复协程是否安全?
具体来说,resuming_on_new_thread()在一个线程启动,co_await后在另一个线程恢复并访问同一个协程“帧”。通常从两个线程访问同一数据需要同步,示例代码如下:
#include <coroutine> #include <iostream> #include <stdexcept> #include <thread> auto switch_to_new_thread(std::jthread& out) { struct awaitable { std::jthread* p_out; bool await_ready() { return false; } void await_suspend(std::coroutine_handle<> h) { std::jthread& out = *p_out; if (out.joinable()) throw std::runtime_error("Output jthread parameter not empty"); out = std::jthread([h] { h.resume(); }); // 潜在未定义行为:访问可能已销毁的*this // std::cout << "New thread ID: " << p_out->get_id() << '\n'; std::cout << "New thread ID: " << out.get_id() << '\n'; // 此操作安全 } void await_resume() {} }; return awaitable{&out}; } struct task { struct promise_type { task get_return_object() { return {}; } std::suspend_never initial_suspend() { return {}; } std::suspend_never final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; }; task resuming_on_new_thread(std::jthread& out) { std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n'; co_await switch_to_new_thread(out); // awaiter在此处销毁 std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n'; } int main() { std::jthread out; resuming_on_new_thread(out); }
进一步考虑函数包含数据的场景:
task resuming_on_new_thread(std::jthread& out) { int data = 0; // 使用线程A存储一些数据。 std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n'; co_await switch_to_new_thread(out); // awaiter在此处销毁 std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n'; std::cout << data << '\n'; // 使用线程B访问data。**这样安全吗?** }
核心疑问
上述场景中,跨线程访问协程帧及其中的data,无需同步是否安全?
解答
这种场景下是安全的,不需要额外的同步机制,原因如下:
协程帧的访问时序保证
协程挂起后,原线程(线程A)会立即退出协程的挂起点,直到协程被恢复前,线程A不会再对协程帧进行任何操作;新线程(线程B)只有通过h.resume()恢复协程后,才会开始访问协程帧。两者的访问是严格的先后关系,不存在并发访问的情况。co_await挂起的语义约束
co_await的挂起过程具有明确的语义:当await_suspend执行完成并返回后,原线程与协程帧的关联就暂时断开,不会再触发对协程帧的读写。新线程的resume()操作是协程帧的唯一后续访问入口,此时协程帧的“使用权”已经完全转移到新线程,不存在数据竞争的可能。局部变量的访问安全性
示例中的data存储在协程帧中,线程A在co_await之前完成了对data的写入操作,之后线程A不会再访问该变量;线程B恢复协程后才开始读取data,属于单写单读的时序访问,符合C++内存模型的安全要求,不需要额外同步。
另外需要注意示例中注释的潜在未定义行为:await_suspend中直接访问p_out的问题,是因为awaitable对象可能在原线程挂起后被销毁,而非协程帧本身的线程安全问题。
内容的提问来源于stack exchange,提问作者trev

