本章导读
Channel 是并发体系里最高频的业务层工具:类型安全的消息传递(ptr 语义)、精确容量与背压(满即等待——容量 0 是会合式同步交接)、Go 同款关闭语义(Close 后发送报错、接收排空存量后才结束——"没有数据丢、没有发送悬空"的停机保障)、Select 多路等待(同时等多个通道与定时器——先到者胜)与可取消接收(RecvCancel——第 54 章令牌的二合一等待)。第 21 章的无锁队列是它的传输底座;Channel 在其上加了关闭协议与等待语义。
57.1引入
生产者消费者用第 53 章 Mutex+Condition 手拼(条件等待+互斥保护+唤醒配合)要三十行且易错(忘记 Signal、虚假唤醒、缓冲边界);用第 21 章无锁队列解决了传输(无锁高吞吐)但没有关闭——消费者怎么知道"不会再有了"?停机时生产者还在 Send 怎么办?
Channel 把两件事打包:传输(容量队列——满则 Send 等待、空则 Recv 等待)与协议(Close 之后的规则:发送方得到明确错误、接收方把存量取完后得到明确结束信号)。关闭语义是消息系统的停机基石——"发送不会再有"是一个事件而不是靠超时猜。Select 再补上多路等待:一个协程/线程同时等两个通道(任一到来)或带定时(超时先到)——事件循环的基本姿势。
57.2概念
通道三件:容量、发送、接收
容量语义:0 是会合式——发送等接收到场、一对一中转(同步交接的标准形态);N 是缓冲式——最多缓存 N 条、满了 Send 等待(背压内建)。TrySend 的用途:非阻塞路径(事件循环里不能堵)的试探——满则失败由调用方决定丢弃或稍后(不是等待)。
关闭语义:Go 同款三方规则
| 角色 | Close 之后 |
|---|---|
| 发送方 | Send/TrySend 返回明确错误——不会再有数据进去 |
| 接收方 | 存量继续可收——排空后 Recv 返回结束信号(非 OK) |
| 关闭方 | Close 幂等——多次关闭安全 |
排空语义(channel 范例的循环写法)是停机的关键:Close 不是"清空"——已入队的数据照常被消费;"关闭"只宣告未来的关闭。这保证了"没有数据丢、没有发送悬空"——第 21 章队列 Close 段落(第 48 章停机协议)的消息版。
Select:多路等待
xrtChannelSelect(select 范例演示):同时注册多个通道的接收意向 + 可选定时——任一先到即返回(带哪个通道到了的标记)。形态:等待数组 + 超时 → 返回就绪项。公平性:多路同时就绪时按注册序或实现策略——不承诺随机公平(需要加权公平在业务层轮转注册顺序)。Select 是"一个消费者服务多个来源"与"带超时的等待"的标准姿势——事件循环、超时控制的主干。
与取消令牌的合流
xrtChannelRecvCancel(channel_cancel 范例):接收与取消二合一——第 54 章可取消等待的 Channel 实例。组合出三路等待:数据到 / 通道关(结束信号)/ 取消命中——返回 xwaitresult 统一报告。Send 侧同样有取消变体(停机时卡在满通道的发送者也能被取消唤醒)。Channel 的全部等待形态(Send/Recv/Select)都可配令牌——取消树穿过通道。
通道 vs 队列:何时用哪个
第 21 章无锁队列与本章 Channel 的分工:队列是传输原语——最高吞吐(无锁)、批量接口、SPSC/MPSC/MPMC 拓扑专用;Channel是协议原语——关闭语义、Select 多路、取消集成、调度器亲和(协程等待不占线程——第 56 章红利)。选型:热路径数据面(网络收发、任务分发)用队列;业务协作面(请求-响应、事件分发、停机排空)用 Channel。两者可组合——Channel 内部用队列做底(实现层)、外部接口各司其职。
57.3示例
完整程序:容量、TrySend 与关闭排空
来自仓库范例 examples/concurrency/channel/main.c:
#include <stdio.h> #include <xrt.h> */ /* 演示精确容量、非阻塞发送和关闭后排空。 */ int main(void) { xchannel* pChannel = xrtChannelCreate(2u); ptr pItem = NULL; if ( pChannel == NULL ) { return 1; } if ( (xrtChannelTrySend(pChannel, (ptr)(uintptr_t)10u) != XCHANNEL_OK) || (xrtChannelTrySend(pChannel, (ptr)(uintptr_t)20u) != XCHANNEL_OK) ) { (void)xrtChannelDestroy(pChannel); return 2; } xrtChannelClose(pChannel); while ( xrtChannelRecv(pChannel, &pItem) == XWAIT_OK ) { printf("%llu\n", (unsigned long long)(uintptr_t)pItem); } return xrtChannelDestroy(pChannel) ? 0 : 3; }
刚才发生了什么。 ① xrtChannelCreate(2) 精确容量 2——两条 xrtChannelTrySend(10、20)都成功入队(未满返回真);第三条会返回假(满——本例未发第三条)。② xrtChannelClose 关闭——存量仍在:两条 Recv 照常取出 10、20(排空);③ 取完存量后 Recv 返回结束信号(非 OK)——接收循环自然终止。④ 这就是"关闭三方规则"的完整实证:发送停(Close 后)、存量走(10/20 被消费)、结束明(排空后的信号)。channel_select 范例演示 Select:两个通道(快与慢)同时等——快者胜(20 来自先到的那路)。
完整程序:Select 多路等待
来自 examples/concurrency/channel_select/main.c:
#include <stdio.h> #include <xrt.h> */ /* 从两个 Channel 中接收先到达的消息。 */ int main(void) { xchannel tFirst; xchannel tSecond; xchannelcase arrCase[2]; xchannelselectresult tResult; ptr pFirst = NULL; ptr pSecond = NULL; int iExit = 0; if ( !xrtChannelInit(&tFirst, 1u) || !xrtChannelInit(&tSecond, 1u) ) { return 1; } if ( xrtChannelTrySend( &tSecond, (ptr)(uintptr_t)20u ) != XCHANNEL_OK ) { iExit = 2; } else { arrCase[0] = xrtChannelCaseRecv(&tFirst, &pFirst); arrCase[1] = xrtChannelCaseRecv(&tSecond, &pSecond); tResult = xrtChannelSelect(arrCase, 2u); if ( (tResult.Wait != XWAIT_OK) || (tResult.Index != 1u) ) { iExit = 3; } else { printf("%llu\n", (unsigned long long)(uintptr_t)pSecond); } } (void)xrtChannelUnit(&tFirst); (void)xrtChannelUnit(&tSecond); return iExit; }
刚才发生了什么。 ① 两个通道各挂一条数据,但到达时序不同——Select 注册两路的接收意向,先到者(发 20 的那路)胜出并被接收。② 20 的输出证明 Select 的裁决:多路中先到先服务——不是轮询两路(那是忙等),是一次等待多个事件。③ 剩下那路的数据还在(未消费——后续 Select 或 Recv 可取);Select 不消费未就绪的路。④ 这个形态放进事件循环:多个来源(定时器、IO 通知、命令通道)一个 Select——事件循环的标准主干。channel_cancel 与 deadline 范例补齐第三维:取消与超时的合成等待。
57.4契约
- 容量即背压:满则 Send 等待;容量 0 会合式(同步交接点);TrySend 是非阻塞探测不是等待。
- 关闭三方:发送方报错 / 接收方排空存量后得结束信号 / Close 幂等——停机排空的标准协议。
- Select 先到先服务:多路同时就绪按实现策略;未就绪路不消费——公平性需求在业务层轮转。
- 取消集成:Send/Recv/Select 全形态可配令牌——三路等待(数据/关闭/取消)统一返回 xwaitresult。
- 消息归属:传
ptr——发送方构造、接收方处理;引用计数对象可直接传(第 31 章值的引用纪律)。 - 队列分工:数据面热路径用第 21 章队列;协作协议面用 Channel——组合不混淆。
从示例到工程:Channel 的三个宿主
任务分派(请求处理形态):接收协程 RecvCancel 等请求 → 处理协程池消费 → 结果通道回写——Channel 串起整条流水线,容量即并发上限(背压)、关闭即停机排空(第 49 章五秒约定的消息版)。事件总线:多个事件源(信号、定时、IO 通知)各自协程产出事件入一个 Channel——消费者一个 Select 兼顾全部源(扇入);按事件类型分发的中心枢纽。Actor 邮箱:每个 Actor 一个命令通道(容量小而确定)、Actor 循环 Recv 处理——天然的串行化(Actor 内无锁);关闭邮箱即 Actor 退休。三宿主共享同一个心智模型:Channel 是并发世界的"方法调用"——传消息代替共享内存、等待代替锁、关闭代替生命周期标志(CSP 学派的箴言在 XRT 的落地形态)。
CSP 与共享内存:两种并发风格
Channel 背后的学派(CSP——Communicating Sequential Processes)值得与第 53 章的共享内存风格正式对照。共享内存派:数据放共享结构、锁保护访问——直接、高性能、但锁的正确性全靠纪律(锁序、粒度、死锁)。消息派:数据不共享、经通道传递归属——无锁心智(单线程持有数据)、组合清晰(通道即接口)、但拷贝/传递有成本与延迟。工程现实是混合:数据结构内部用锁或无锁原语(第 21 章队列)、模块间协作用消息(Channel)——"内部紧凑、外部清晰"。XRT 两种都给足(第 53 章原语 + 本章 Channel),选型由层决定而不是信仰。
一个设计观:容量的三个数字
通道容量不是一个数——是三个数的设计决策。0:会合式——要"对方确认收到"的同步交接(RPC 语义);小确定值(1~16):背压快速传导——上游几乎立即感知下游慢(容量小即告警线,过载早暴露);大值或专用队列:削峰填谷——突发流量缓冲(但要配观测:队列深度曲线失控前可见)。三个数字对应三种承诺:同步、早背压、削峰。经验起点:协作通道用小值(8)、请求管道看下游延迟与内存预算、永远不给"无限"(第 21 章背压阀的纪律在 Channel 同样成立——CreateLimit 的投递上限是调度器侧的姊妹机制)。
常见错误
症状:关闭后接收方"偶尔丢数据"——实际上 Close 之前的存量没有被排空就退出了接收循环;或发送方在 Close 后的数据静默消失。
原因:把 Close 理解成"作废通道"——实际它是"宣告未来关闭";存量照常可收、收完才有结束信号。
xrtChannelClose(pChannel); /* 接收方以为关闭即无数据——直接退出,存量丢失 */
xrtChannelClose(pChannel); /* 排空循环:存量继续收,直到结束信号 */ while ( xrtChannelRecv(pChannel, &Item) == XWAIT_OK ) { Consume(Item); /* 存量期间返回 OK;排空后返回结束信号 */ } /* 结束信号后退出——数据零丢失(范例的写法) */
症状:容量 0 的通道上 Send 后立即做下一步——数据"看起来发出去了"但接收方还没到场,吞吐远低于预期或时序错乱。
原因:容量 0 是会合式——Send 等接收方到场完成交接;把它当"异步缓冲"用,语义与预期错位。
xchannel* Ch = xrtChannelCreate(0); /* 会合式 */ xrtChannelSend(Ch, Item); /* 等接收方到场——"发后即走"的预期落空 */ DoNextThing(); /* 时序依赖没发生 */
xchannel* Ch = xrtChannelCreate(8); /* 缓冲式:8 条余量 */ xrtChannelSend(Ch, Item); /* 入队即返回(未满)——发后即走成立 */ DoNextThing(); /* 要同步交接(确认对方收到才继续)才用容量 0——按语义选容量 */
编程练习
容量 3 的通道:入 3 条 → Close → 排空收 3 条 → 第 4 次接收得到结束信号;再对已关闭通道 Send 验证报错——三方规则逐条断言。
三个生产者协程(各产 5 条带标号的消息)汇入一个 Channel,消费者 Select 兼顾通道与定时器(2 秒超时)——统计收满 15 条与超时路径各验证一次。
完整请求管道:接收协程(RecvCancel 等请求或取消)→ 处理协程(Park 等外部 DB 回调 Wake)→ 响应通道;停止信号触发取消树,全管道排空收尾。验收标准:停机时在途请求零丢失(要么处理完要么明确取消);各协程终态与排空顺序可审计;第 50 章观测字段齐全。
小结
| 知识点 | 速查 |
|---|---|
| 容量语义 | 0 会合式(同步交接)/ N 缓冲式(满则等=背压);TrySend 非阻塞 |
| 关闭三方 | 发送报错 / 接收排空后结束信号 / Close 幂等——Go 同款 |
| Select | 多路接收+可选定时,先到先服务;未就绪路不消费 |
| 取消集成 | Send/Recv/Select 可配令牌——三路等待统一 xwaitresult |
| 消息归属 | ptr 传递——发送方构造接收方处理;引用计数对象直接传 |
| 与队列分工 | 数据面热路径队列 / 协作协议面 Channel——组合不混淆 |
📖 本章涉及的 API 参考手册:channel(40 个条目)cancel(18 个条目)
点击查看完整签名、参数约束、返回值与错误说明