代表性面试主题

C++ 面试题:用 std::jthread 和 stop_token 实现可协作取消

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

题干

请用 C++20 实现一个 worker:它循环消费任务,主线程可以请求停止;停止时不能丢失已开始任务、不能永久阻塞,也不能在对象析构后访问共享状态。请说明 std::jthread、stop_token、condition_variable_any、异常和测试策略。

题干与适用场景

你需要实现一个后台 worker,持续从队列读取任务。服务关闭或上游取消时,主线程请求 worker 停止;已经开始的任务要完成,尚未开始的任务可以保留或明确丢弃。实现必须避免忙等、数据竞争、析构时遗留线程和条件变量永久等待。

C++20 的 std::jthread 会在析构时请求停止并 join;它可以向入口函数注入 std::stop_token。停止请求是协作信号,不会强制终止任意代码;worker 必须轮询或使用可响应 stop token 的等待。

面试官考察点

  • 是否理解 stop token 是共享状态上的请求,不是异步杀线程。
  • 是否使用 std::jthread 的自动 join,并避免在线程仍访问对象成员时析构对象。
  • 是否让阻塞等待能被停止唤醒,例如 condition_variable_any::wait 的 stop-token 重载。
  • 是否保证队列、停止状态、任务异常和资源清理的生命周期安全。
  • 是否测试停止竞态、空队列、任务抛异常、重复 request_stop 和析构顺序。

回答前需要澄清的问题

  • 停止时正在执行的任务能否完成,任务是否幂等或有外部副作用?
  • 队列关闭后,未消费任务是丢弃、转移还是等待另一个 worker?
  • worker 的等待是否支持 condition_variable_any,还是依赖不可中断的第三方 I/O?
  • 异常由 worker 记录、传递给 join 方,还是触发整体关闭?
  • 对象是否可能从多个线程调用 stop、析构或重新启动?

30 秒回答框架

“用 std::jthread 持有 worker,让入口接收 std::stop_token。队列用互斥锁保护,等待时使用能响应 token 的 condition_variable_any;唤醒后先判断 stop、队列关闭和任务状态。已经取出的任务运行到明确的取消检查点,再释放资源。析构依靠 jthread 的 request_stop 加 join,禁止 detach。所有共享状态的生命周期覆盖线程,并测试停止竞态和异常。”

分步骤深入解答

第一步:定义取消契约

把停止分为“请求停止”和“任务完成”。stop 请求只阻止 worker 获取新任务;已开始任务在安全检查点完成或返回可识别的取消结果。不能承诺任意第三方调用会立即终止。

第二步:设计线程与状态所有权

让拥有队列、互斥锁、条件变量和 std::jthread 的对象活得比线程久。构造 jthread 后再发布对象指针;析构先请求停止并等待,再释放成员。禁止捕获已经离开作用域的引用,禁止把裸 this 交给可能晚于析构运行的回调。

第三步:让阻塞等待可取消

使用 condition_variable_any 的 stop-token 等待,或注册 stop_callback 调用 notify_all。谓词同时检查队列非空、队列关闭和 stop_requested();被唤醒后重新持锁判断,不能把一次通知当作状态保证。

cpp
std::jthread worker([this](std::stop_token st) {
  for (;;) {
    Task task;
    {
      std::unique_lock lock(mu_);
      cv_.wait(lock, st, [this, &st] {
        return closed_ || !queue_.empty() || st.stop_requested();
      });
      if (st.stop_requested() || (closed_ && queue_.empty())) return;
      task = std::move(queue_.front());
      queue_.pop_front();
    }
    run(task, st);
  }
});

第四步:处理任务中的停止点和异常

长任务在分段操作之间检查 token;检查点要定义副作用边界,避免半写入。捕获异常后记录任务 ID 和错误,决定是否继续消费或请求整体停止;不要让异常穿出线程入口导致 std::terminate

第五步:处理关闭顺序

关闭流程先禁止新任务,再通知队列和 stop token,等待 worker 退出,最后释放资源。若要排空队列,不能在 stop 请求后继续无界接收;明确 drain 超时和剩余任务处理。重复 request_stop() 应安全且不重复触发副作用。

第六步:验证竞态和可观测性

测试空队列停止、任务刚出队时停止、多个线程同时停止、析构期间通知、任务抛异常、阻塞 I/O 超时和重复关闭。记录停止延迟、完成/取消任务数、队列剩余、异常和 join 时间;用 ThreadSanitizer 检查数据竞争。

高质量示范回答

“我用 jthread 管理 worker 生命周期,入口接收 stoptoken。队列状态由 mutex 保护,conditionvariable_any 的谓词检查关闭、非空和 stop 请求;停止会唤醒等待。worker 取出任务后释放锁,任务在安全检查点检查 token,完成必要的事务收尾后返回。异常在入口捕获并记录,不穿出线程。”

“关闭时先拒绝新任务,再设置 closed、notifyall、requeststop,最后 join。析构不会 detach,也不会提前释放队列或日志器。测试覆盖空队列、出队竞态、异常、重复停止和长任务超时,并观察 join 延迟、剩余任务和数据竞争。”

常见错误

  • 把 stop_token 当强制杀线程 → 破坏资源和事务 → 定义协作检查点。
  • std::thread 后忘记 join → 析构触发终止或线程悬挂 → 用 jthread 或显式生命周期管理。
  • 条件变量只等待一次通知 → 丢通知后永久睡眠 → 用谓词循环并让 stop 唤醒。
  • 任务持锁执行 → 停止和生产者被阻塞 → 出队后释放锁。
  • 异常穿出线程入口 → 进程终止 → 在线程边界捕获并记录策略。
  • 析构先释放成员再停线程 → use-after-free → 先 stop、join,再释放状态。

追问及应对

jthread 析构会做什么?

若仍可 join,析构会请求停止并 join;它不会强制终止任务。任务必须响应 stop,join 可能等待其到达安全检查点。

stop 请求能唤醒 condition_variable 吗?

使用支持 stop token 的 condition_variable_any 等待时,停止请求会使等待返回;自定义等待则需用 stop_callback 通知,并在谓词中重新检查状态。

正在执行的数据库写入能立即取消吗?

不能假设。应把写入拆成可回滚或幂等步骤,等待驱动超时/取消能力,在提交边界检查 stop;否则返回明确的未知状态并由恢复流程处理。

如何避免停止竞态?

把 closed、队列和 stop 视为同一生命周期协议,所有状态转换在锁下完成,通知放在状态改变之后;用压力测试和 ThreadSanitizer 覆盖停止与出队同时发生的窗口。

公开来源

同类题目

相关面试工具

用 Screenshot 处理算法题

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

查看工具