发布于2026-07-14 阅读(0)
扫一扫,手机访问
来说几个C++20协程实现异步I/O调度器时的核心判断。很多人在这个坑里栽过跟头,问题通常不是协程挂起和恢复本身,而是promise、awaiter、OS I/O句柄和调度队列这四个关键组件的生命周期必须严格对齐。稍微漏掉一环,表现出来就是偶发crash或者内存泄漏,而且这种bug往往极难复现。调试时优先检查resume调用点是否还在promise有效期内,其次确认所有new出来的awaiter最终都被正确delete。其他细节都可以往后放。

直接把 std::coroutine_handle 往队列里一丢,等I/O完成后再去恢复?十有八九会crash。C++20协程的 awaiter 是栈对象,挂起后如果它所在的栈帧已经退出——比如函数返回了——再调用 await_resume() 时,就会访问到悬垂指针。
正确的做法是:在 await_suspend() 中把 coroutine_handle 通过 std::coroutine_handle::from_address() 转存为裸指针或 std::shared_ptr 管理的句柄;同时确保 awaiter 本身是堆分配的(比如用 new awaiter{handle}),或者绑定到调度器生命周期内有效的对象上。
std::vector> m_pending ,并在 await_suspend() 返回前调用 handle.promise().set_state(PENDING),让promise自己管理自身的存活状态。return handle; 后紧接着就return函数——这会立刻析构awaiter,但handle可能还没被调度器接管。单线程调度器用 epoll_wait() 阻塞等待,但多个协程可能同时发起read/write,这就需要一套线程安全的入队机制,把就绪事件跟对应的协程关联起来。直接用 std::mutex 包裹整个事件循环?它会迅速成为瓶颈。
更实际的方案是:I/O发起线程——也就是协程挂起的地方——把待注册的fd和 coroutine_handle 封装成结构体,通过 std::atomic 指针或 moodycamel::ConcurrentQueue(轻量级无锁队列)投递到epoll线程;epoll线程批量调用 epoll_ctl(ADD),再统一执行 epoll_wait()。
O_NONBLOCK,否则 read() / write() 在 await_suspend 过程中可能意外阻塞主线程。epoll_event.data.ptr 存储指向 awaiter 或 promise 的指针,省去查找开销。handle.done() == false,防止已被销毁的协程被误唤醒。默认的 promise_type 不提供取消语义,而真实I/O场景里,cancel、timeout、retry是家常便饭。必须显式支持:使用 await_transform(std::stop_token),或者自定义 async_read_with_timeout() 返回一个带cancel支持的awaitable。
典型结构是promise内部维护一个 std::optional,在 initial_suspend() 返回 suspend_always{} 之后,由调度器在合适的时机调用 m_stop_src->request_stop();对应的awaiter在 await_ready() 中轮询 stop_token.stop_requested(),或者结合timerfd实现超时唤醒。
std::jthread 自动join来“保证”协程结束——协程可能卡在I/O挂起态,jthread退出时handle未resume,promise析构直接触发未定义行为。co_await 才能响应,因此业务代码需要定期检查 co_await std::stop_token{}。IOCP 的 GetQueuedCompletionStatus() 返回的是 OVERLAPPED*,而不是fd。很多移植代码直接把 OVERLAPPED 成员变量当成协程handle来存,结果resume时崩溃——因为 OVERLAPPED 是栈分配或临时对象,I/O完成时它早就析构了。
正确的做法:把 coroutine_handle 存进自定义的 OVERLAPPED 派生结构体(比如 struct io_op : OVERLAPPED { std::coroutine_handle h; };),在 await_suspend() 中用new分配这个结构体,再传给 WSARecv() 等API;I/O完成后从 LPOVERLAPPED 转回 io_op*,再调用 h.resume()。
WSA_IO_INCOMPLETE)时,务必调用 h.destroy() 而不是 h.resume(),否则协程栈会被重复析构。co_await 文件读写必须用 FILE_FLAG_OVERLAPPED 创建句柄,普通HANDLE不支持异步I/O。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8