第23章:协程
为什么有这一章
前面章节里的每个服务器都有同一个隐藏限制:一个线程一次只能做一件事。同步 echo 服务器接受一个 客户端,写给它,关闭它,然后才接受下一个。慢客户端会阻塞服务器。通常的答案是每连接一个线程, 这在连接数超过机器能负担的线程数之前都有效。
协程是另一个答案。协程是一个可以在中间暂停、之后再恢复的函数,等待时不拥有线程。代码仍然看起来像 一串语句:接受、读、写、循环。由调度器而不是程序员决定下一个运行哪个暂停的协程。
这一章先讲语言机制,再讲 fast_io 的异步模型,最后写一个真实的 TCP 服务器。最后一页把 echo 服务器 变成 HTTP 服务器,在那里格式化 I/O 与裸字节传输相遇。
四页
简短版本
C++20 协程是含有 co_await、co_yield 或 co_return 的函数。
编译器把它变成一个帧加一个 promise。帧保存参数、跨越暂停点的局部变量和恢复点。promise 决定协程
返回什么、是否惰性启动、结束时发生什么,以及错误如何保存。
co_await 使用 awaiter 协议:await_ready、await_suspend、
await_resume。异步 I/O 就是这个协议加一个调度器。协程提交操作并暂停;事件循环等待
内核,然后恢复保存的句柄。代码读起来像阻塞序列,但操作在途时线程是自由的。
fast_io 的异步层是四个对象:拥有型 io_async_scheduler、非拥有型
io_async_observer、惰性 io_async_task<T>,以及
io_async_wait 事件泵。调度器在 Linux 上是 io_uring,在 Windows 上是 IOCP,
在其他 POSIX 系统上是线程池。源码不变。
结构化输入用 async_scan,结构化输出用 async_print,字节移动用
async_transmit_some_bytes / async_transmit_all_bytes。只有在你实现
这些操作下面那一层时,才降到 async_pread_some_bytes /
async_pwrite_some_bytes。读行的服务器不应该调用 pread。
协程不是线程。暂停是协作式的;在协程里阻塞会阻塞同一事件循环上的所有协程。帧有生命周期:
让拥有任务活到 done(),或者 detach() 它,让它自行销毁。错误通过
unhandled_exception 或 unhandled_herbception 传递,并在
co_await 处或由 rethrow_if_error() 重新抛出。
服务器模式是两个协程:拥有监听器的 accept 循环,以及每个连接一个 detached 会话。
async_accept 返回拥有的 socket;按值传递把所有权转移进会话帧。监听器死亡时,
监督者重启 accept 循环。这就是整个架构,它能扩展是因为大多数会话在暂停,而不是因为程序创建了更多线程。
要点
- 协程是状态机,不是线程。
co_await挂起当前操作;调度器负责恢复。- fast_io 的异步模型使用
io_async_task和async_*操作。 - 字节用
async_transmit,值用async_scan/async_print。