题目与背景
C++26 将 std::execution sender/receiver 模型纳入标准化方向。给定读取网络块、解析记录、批量写盘三个阶段,请组合成一个 sender pipeline。调用方可以请求停止,也可以把不同阶段放到 I/O 或 CPU 调度器;要求说明值、错误、停止三条完成通道和资源清理。
面试官考察什么
重点是区分惰性的 sender 描述与连接后的 operation state,理解 connect 只建立状态而 start 才启动工作。答案还要说明 receiver 的 setvalue、seterror、set_stopped 如何沿管道传播,调度器切换是否改变线程亲和性,以及取消时如何避免提交新的副作用。
先问清楚的澄清问题
任务与背压
确认每批大小、允许的并发数、是否必须保持输入顺序,以及写盘失败后是否允许重试。背压策略决定是限制 sender 并发,还是由队列容量施加上限。
调度与停止来源
确认 I/O 和 CPU scheduler 的接口、停止请求来自超时还是用户操作,以及停止后已提交的系统调用如何收尾。停止请求不等于强制杀线程。
资源与提交语义
确认文件句柄、缓冲区和临时文件的所有权,写入是否幂等,部分批次是否需要回滚。这样才能界定 set_error 后允许的清理动作。
30 秒回答框架
“我先用 sender adaptor 描述读取、解析和写入,再用调度器 adaptor 切换执行上下文。pipeline 保持惰性,connect 生成 operation state,start 才开始。每个阶段把成功值交给下游,异常走 seterror,停止请求走 setstopped;共享 stop token 贯穿所有阶段。停止路径在提交副作用前检查 token,已提交的 I/O 由拥有者完成关闭和临时文件清理。”
深入解答步骤
第一步:定义值和错误边界
为每个阶段定义输入输出类型,并把可恢复错误转换为显式 error sender。不要用异常跨越 scheduler 边界;最终 receiver 统一记录成功、失败或停止。
第二步:组合惰性 pipeline
用 let_value、then 或等价 adaptor 连接读取、解析、写入。组合阶段只构造描述,不分配线程,也不执行 I/O;需要共享状态时让 operation state 持有生命周期。
第三步:连接并启动
调用 connect(sender, receiver) 得到 operation state,把它保存到仍存活的作用域,再调用 start。receiver 必须比 operation state 活得久,异步回调不能引用已销毁的栈对象。
第四步:切换执行上下文
在 I/O 完成后用 scheduler sender 把解析阶段迁移到 CPU 线程池,写盘阶段再回到受控的 I/O 线程。记录 scheduler 的队列容量和公平策略,避免把阻塞写操作放入无限制的通用池。
第五步:传播停止和背压
将 stop token 传给每个可中断阶段。收到停止后,新的批次不再入队,已在运行的系统调用按 API 能力取消或等待完成;队列满时通过限流 sender 暂停上游,防止内存无界增长。
第六步:处理错误和部分副作用
写盘前先写临时文件或记录批次序号,成功后原子提交。set_error 触发下游清理并关闭句柄;重试必须有上限和幂等键,避免重复写入。
第七步:验证并发与生命周期
测试多 scheduler、停止竞态、解析异常、写盘短写和 receiver 提前销毁。用线程分析器检查数据竞争,统计排队时间、吞吐、停止延迟和未释放 operation state 数量。
高质量示例回答
我会让读取 sender 产生批次,解析 sender 在 CPU scheduler 执行,写入 sender 在 I/O scheduler 执行。pipeline 只描述依赖;connect 创建 operation state,start 才启动。每阶段都实现值、错误、停止三条路径,并共享 stop token。停止时阻止新批次入队,已提交 I/O 完成关闭;写入使用临时文件和批次序号保证重试幂等。测试覆盖 scheduler 切换、背压、停止竞态和 receiver 生命周期。
常见错误
- 错误: 构造 sender 就认为任务已经运行。→ 原因: sender 是惰性描述。→ 改进: 明确 connect 和 start 的启动边界。
- 错误: 只处理异常,不处理停止完成信号。→ 原因: 停止是独立完成通道。→ 改进: receiver 同时实现 seterror 与 setstopped。
- 错误: 停止时直接杀线程。→ 原因:线程可能持有文件句柄或半写入状态。→ 改进: 传播 stop token,按阶段安全收尾。
- 错误: 无界地把批次提交到线程池。→ 原因: 缺少背压会耗尽内存。→ 改进: 限制并发、队列容量和重试次数。
追问与回答
追问 1:为什么不直接用 future?
sender/receiver 把调度、取消和三种完成通道作为可组合结构,能在连接时统一处理生命周期;future 通常需要额外约定停止和错误传播。
追问 2:start 调用后能否立即销毁 sender?
可以销毁临时 sender 描述,但 operation state、receiver 及其捕获的资源必须保持有效,直到收到完成信号。实际所有权应由任务对象或作用域管理。
追问 3:set_stopped 是否代表所有副作用都撤销?
不代表。它表示计算以停止完成;已经提交的 I/O 可能无法回滚,必须依靠临时文件、幂等提交或补偿操作保证一致性。
追问 4:怎样证明不会出现重复写入?
为每批生成稳定序号,提交前检查已完成记录,重试只允许写入缺失序号。测试故障注入、进程重启和停止竞态,验证提交日志与最终文件一致。