本章导读
协程三章的上册讲原语层:xrtCoCreate 创建(有栈——切换回来从让出点继续)、xrtCoResume 驱动到下一个暂停点、xrtCoYield 让出;四态(READY/RUNNING/SUSPENDED/DONE)+ 三终态(RETURNED/CANCELLED/ERROR)刻画完整生命周期;清理栈(CleanupPush/Pop 与 Defer)保证任意退出路径的逆序收尾;协程自带独立错误槽与内建 arena(第 7 章)。后端自动选择 Windows Fiber / POSIX 手写上下文切换(四架构支持)。多数业务代码用中册的调度器而非本章原语——但原语是理解一切上层的地基。
55.1引入
回到第 53 章的"消费者等待"问题:线程在 Condition 上睡——一个等待占一个线程(1MB 栈的系统成本);一千个并发等待就是一千个线程,内存与调度双双爆炸。协程换一个模型:等待时不是睡在原地,而是把执行权交出去、整个"任务"挂起——代价只是几十 KB 的协程栈;一千个挂起的协程只占几十 MB。唤醒时恢复上下文从暂停点继续——逻辑上像"睡",物理上是"让"。
这个模型的工程红利在 IO 密集场景:网络服务里成千上万的连接各自"等数据"——线程模型要么线程爆炸、要么 select 轮询复杂化;协程模型每个连接一个协程、等数据时让出、数据到时恢复——写同步风格的代码,得异步的伸缩性。卷七的网络引擎正是这么用它的。
55.2概念
生命周期:四态三终态
Resume 驱动到下一个暂停点是理解协程的关键句:一次 Resume 推动协程跑到下一个 Yield 或结束——不是"执行完"而是"执行到"。金标准章(ch4 错误模型)演示过三段式:创建后三次 Resume(第二次后 SUSPENDED、第三次 DONE+RETURNED)。协程过程签名 ptr(ptr)——返回值经 xrtCoResult 借用读取。
让出与协作取消
xrtCoYield() 返回 xwaitresult——正常让出返回 OK、取消请求时返回 CANCELLED:协程在让出点感知取消、清理后返回(终态 CANCELLED)。xrtCoCancel 只是标记(与第 54 章令牌同款协作语义)——真正的终止发生在协程自己的下一个让出点;xrtCoCurrent 判断"我在不在协程里"(普通代码调 Yield 是未定义——先判再让)。取消令牌族(第 54 章)经调度器接入协程体系——中册展开。
清理栈:任意退出路径的保证
协程内的资源(打开的句柄、借用的缓冲)用清理栈托管:xrtCoCleanupPush/Pop 手动管理(栈上节点零开销)或 xrtCoDefer 自动管理;协程无论正常返回、取消还是出错——清理栈都逆序执行(与 Go defer 同款保证)。这与第 7 章 arena 的"整段回收"互补:arena 管内存、清理栈管动作(关句柄、发通知这类非内存收尾)。
独立错误槽与内建 arena
协程切换自己的错误槽(第 4 章的并发版完全体):协程 A 的错误不会污染承载线程或其他协程——迁移协程不丢错误上下文;协程内照常用 xrtGetError/xrtTakeError 操作的是自己的槽。内建 arena:协程创建即带独立 temp arena,结束自动回收——第 7 章"请求级临时内存"的协程版(协程就是"请求")。
线程绑定:协程属于创建线程
协程固定归属创建它的线程——别的线程 Resume 是未定义。跨线程协作走调度器 Post(中册)或消息传递;这条纪律与有栈协程的上下文实现(TLS/栈守卫)绑定,不是建议是硬性。
55.3示例
完整程序:两段协程与让出
来自仓库范例 examples/concurrency/coroutine/main.c——59 行走完创建/让出/终态:
#include <stdio.h> #include <xrt.h> /* 示例协程分两段完成工作。 */ static ptr exampleCoroutine(ptr pData) { int* pValue = (int*)pData; *pValue += 1; if ( xrtCoYield() != XWAIT_OK ) { return NULL; } *pValue *= 2; return pValue; } */ /* 展示低级协程的创建、恢复和结果读取。 */ int main(void) { int iValue = 20; xcoro* pCo = xrtCoCreate(exampleCoroutine, &iValue, NULL); if ( pCo == NULL ) { return 1; } (void)xrtCoResume(pCo); printf("after yield: %d\n", iValue); (void)xrtCoResume(pCo); printf("result: %d\n", *(int*)xrtCoResult(pCo)); (void)xrtCoDestroy(pCo); (void)xrtCoThreadDetach(); return 0; }
刚才发生了什么。 ① 协程过程分两段:前半 *pValue += 1(20→21)、Yield 让出、后半 *pValue *= 2。② 第一次 Resume:跑到 Yield 暂停(SUSPENDED)——此时 after yield: 21 打印(21 是前半的成果——让出点即观察点,调用方在此看到中间状态)。③ 第二次 Resume:从让出点继续、跑完返回(DONE + RETURNED)——result: 42 是 xrtCoResult 借用读取的返回值(21×2)。④ 三次交互(Resume/Resume/读结果)的节奏就是协程原语的全部手感——调度器(中册)把这三拍自动化。
完整程序:调度器雏形
来自 examples/concurrency/coroutine_scheduler/main.c——多协程交替执行:
#include <xrt.h> #include <stdio.h> /* 示例任务异步等待后打印编号。 */ static ptr exampleTask(ptr pData) { int iTask = (int)(intptr_t)pData; (void)xrtCoSleep((uint64)iTask * 1000u); printf("task %d\n", iTask); return pData; } /* 调度器 post 在所属线程中创建真正的协程任务。 */ static void examplePost(xcosched* pSched, ptr pData) { (void)xrtCoGo(pSched, exampleTask, pData, NULL); } */ /* 先投递多个轻量任务,再运行到投递和协程全部完成。 */ int main(void) { xcosched* pSched = xrtCoSchedCreate(); if ( pSched == NULL ) { return 1; } for ( int i = 1; i <= 3; i++ ) { if ( !xrtCoSchedPost( pSched, examplePost, (ptr)(intptr_t)i ) ) { return 2; } } if ( !xrtCoSchedRun(pSched) ) { return 3; } (void)xrtCoSchedDestroy(pSched); (void)xrtCoThreadDetach(); return 0; }
刚才发生了什么。 ① 三个协程提交进调度器——task 1/2/3 依次输出展示交替执行:不是顺序跑完一个再跑下一个,而是各跑到暂停点轮转(具体节奏取决于调度策略——中册展开)。② 这就是"协作式多任务"的最小样:单线程、零锁、三个"并发"任务——协程的并发不需要多核,只需要交替点。③ 调度器把本章的 Create/Resume 手工节奏封装成 Post/Run 自动节奏——业务代码写"过程"、调度器管"驱动"。coroutine_tour 与 coroutine_event 范例是全接口与协程事件的扩展阅读。
55.4契约
- 驱动语义:Resume 驱动到下一个暂停点(Yield)或终态;不是执行完。
- 终态三分类:RETURNED/CANCELLED/ERROR;Result/Error 是借用——终态后立即读,Destroy 前用完。
- 线程绑定:固定归属创建线程;跨线程走调度器 Post 或消息传递——硬性纪律。
- 协作取消:Cancel 标记、Yield 感知、清理后 CANCELLED 终态;递归进入自身返回 XERR_STATE。
- 清理栈:逆序执行覆盖全部退出路径;arena 管内存、清理栈管动作——互补不互代。
- 独立错误槽:协程错误不污染承载线程;内建 arena 随协程回收。
- Destroy 时机:只对未启动或已结束的协程成功——活跃协程先 Cancel 驱动到终态。
协程与线程的对照表
协程与线程是并发工具箱的两极,一张对照表定分寸。成本:线程 1MB 栈 + 内核调度对象;协程几十 KB(可配)+ 用户态切换(纳秒级)。抢占:线程可被抢占(调度器说了算);协程只能协作让出(代码说了算——所以不存在协程级的"执行到一半被撕开")。并行:线程真并行(多核同时跑);协程单线程内交替(并行要靠"多线程 × 每线程多协程"的组合——中册的调度器模型)。适用:CPU 密集并行用线程(或任务池);IO 密集高并发用协程(等得多跑得少);两者常组合——每线程一个调度器、每调度器千百协程。心智模型:线程共享内存要同步(第 53 章);协程交替执行在同一线程内——协程之间的共享在很多场景天然互斥(让出点就是边界),但跨线程的调度器实例之间仍要同步。
从示例到工程:协程的三个宿主
网络连接处理(卷七主场):每连接一个协程——读到 EOF 为止的直线逻辑(读、解析、应答循环),等待时让出、数据到时恢复;一千连接 = 一千协程 + 一两个线程——这是协程存在的第一理由。异步流程编排:多步骤异步流程(先查缓存、miss 查库、回写、响应)写成直线协程——每步 await 让出,恢复后接着写;与 Future 链(第 58 章)相比,协程版有局部变量与栈——状态不用显式存在闭包里。生成器与惰性序列:读取大文件的分块处理——每 Yield 产出一块,消费方控制节奏(背压的天然形态)。三宿主的共同判据:"等待多、逻辑想写成直线"——协程的手感优势就在直线二字。
有栈与无栈:一个工程视角
协程实现的两大门派值得一个工程视角的对照。有栈(XRT 选型):每个协程独立栈——任意深度的调用链里都能让出(框架代码里 Yield 也行)、局部变量天然保留、调试器能看到真栈;代价是每协程的栈内存(几十 KB 起)与上下文切换的栈交换。无栈(C++20 coroutine / 状态机编译):编译器把函数拆成状态机——零栈内存、切换近乎免费;代价是只有协程函数本体能让出(调用链里的函数不能)、局部变量放状态机(生命周期受限)、调试看到的是拆解后的机器。XRT 选有栈的理由:C 语言没有编译器魔法可用,手写状态机的复杂度转嫁给每个用户不可接受——有栈的"任意深度让出 + 真栈调试"是 C 工程的务实解。这个视角也解释了为什么协程栈大小可配(第 52 章线程栈的同款考量)。
常见错误
症状:偶发崩溃或状态错乱——协程在别的线程被驱动;复现依赖线程时序。
原因:协程固定归属创建线程(上下文实现绑定 TLS 与栈守卫)——跨线程驱动是未定义行为。
/* 线程 A 创建 */ /* 线程 B 驱动 */ xcoro* Co = xrtCoCreate(proc, Data, &Config); xrtCoResume(Co); /* B 驱动 A 的协程——未定义 */
/* 跨线程协作走消息传递:A 的协程在 A 线程被驱动 */ /* 线程 B 想触发 → 向 A 的队列/通道发消息(第 21/57 章) */ /* 或用调度器的 Post 把"驱动责任"留在正确的线程(中册) */
症状:资源释放顺序错乱——先关了句柄又有人用它;依赖顺序的收尾偶发失效。
原因:手动 CleanupPop 乱序弹出,或混用 Defer 与手动 Pop 导致执行顺序偏离入栈逆序。
xrtCoCleanupPush(&Node1, closeHandle, H1); /* 先入栈:应最后执行 */ xrtCoCleanupPush(&Node2, releaseBuffer, B); /* 后入栈:应先执行 */ xrtCoCleanupPop(&Node1); /* 手动先弹了 Node1——逆序被打破 */
xrtCoCleanupPush(&Node1, closeHandle, H1); xrtCoCleanupPush(&Node2, releaseBuffer, B); xrtCoCleanupPop(&Node2); /* 后入先出——逆序保持 */ xrtCoCleanupPop(&Node1); /* 或全用 Defer:让协程机制保证逆序(推荐——少一个手滑维度) */
编程练习
用 Resume/Yield 实现整数序列生成器:每次 Resume 产出一个值(共享变量或 Result),第十次后 DONE——金标准章练习的协程原语版。
两个协程 + 一个缓冲变量:生产者 Yield 交出时留数据、消费者 Yield 交回时取走——零锁的协作交替;体会"并发不需要多核"。
三级协程流水线(读/算/写),清理栈托管各级资源;取消令牌命中后三级全部干净收尾(终态 CANCELLED、清理栈全执行、arena 回收)。验收标准:任意时刻取消后资源零泄漏(第 6 章统计);终态与清理执行顺序进结构化日志可审计。
小结
| 知识点 | 速查 |
|---|---|
| 三拍节奏 | Create(READY)→ Resume×N(跑到 Yield 或 DONE)→ 终态读 Result |
| 四态三终态 | READY/RUNNING/SUSPENDED/DONE;RETURNED/CANCELLED/ERROR |
| Yield 语义 | 让出点继续;返回 CANCELLED 表示取消命中——清理后返回 |
| 清理栈 | Push/Pop 手动 或 Defer 自动;逆序执行覆盖全部退出路径 |
| 独立设施 | 错误槽隔离(迁移不污染)+ 内建 arena(随协程回收) |
| 线程绑定 | 固归属创建线程——跨线程走调度器 Post 或消息(硬性) |
| Destroy 门槛 | 只对未启动或已结束成功——活跃先 Cancel 到终态 |
📖 本章涉及的 API 参考手册:coroutine(58 个条目)
点击查看完整签名、参数约束、返回值与错误说明