代表性面试主题

C++26 面试:sender、receiver 与 operation state 如何协作?

编程题困难
Offer.cc 编辑团队发布 更新

题干

请解释 C++26 std::execution 中 sender、receiver 和 operation state 的职责。connect 与 start 分别发生什么,值、错误和停止如何完成?

题目与背景

本题考察 C++26 std::execution 的核心对象模型。请从一个最小异步操作出发,解释 sender 的惰性描述、receiver 的完成契约、connect 产生的 operation state,以及 start 何时让计算真正开始。

面试官考察什么

重点是区分惰性的 sender 描述与连接后的 operation state,理解 connect 只建立状态而 start 才启动工作。答案还要说明 receiver 的 set_valueset_errorset_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_valuethen 或等价 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:怎样证明不会出现重复写入?

为每批生成稳定序号,提交前检查已完成记录,重试只允许写入缺失序号。测试故障注入、进程重启和停止竞态,验证提交日志与最终文件一致。

公开来源

同类题目

相关面试工具

用 Screenshot 处理算法题

截图题目后,按顺序看约束、解法、代码、边界条件和复杂度。

查看工具