当前位置: 首页 > ops >正文

线程池八股文


🧾 线程池八股文


一、基础语法 & STL

Q1:std::function<void()> task; 在这里是什么意思?

  • workers_ 的线程循环里,定义了一个 task 变量,它是一个 通用可调用对象包装器

  • 这里统一把所有提交的任务(lambda、函数等)存放到 tasks_ 队列中,工作线程取出来放到 task 里,再调用 task() 执行。


Q2:为什么要 std::move(tasks_.front())

  • 因为 tasks_ 是一个 std::queue<std::function<void()>>

  • tasks_.front() 返回的是引用,如果直接赋值会触发一次拷贝构造;

  • std::move 可以把任务对象的所有权转移到局部变量 task,避免额外拷贝,提高效率。

  • 注意:不能返回引用,因为 tasks_.pop() 后,front() 指向的元素生命周期就结束了。


Q3:workers_.emplace_back([this]{ ... }) 为什么用 emplace_back

  • workers_ 是一个 std::vector<std::thread>

  • emplace_back 可以在 vector 内部直接构造一个 std::thread 对象,避免一次多余的拷贝/移动。

  • 在这里,lambda 捕获了 this,所以每个线程都能访问 tasks_queue_mutex_condition_


Q4:为什么在 condition_.wait() 里必须用 unique_lock 而不是 lock_guard

  • condition_.wait() 内部会在阻塞期间 释放 queue_mutex_,唤醒后再重新加锁。

  • lock_guard 不能解锁再上锁,所以必须用 std::unique_lock<std::mutex>


二、并发编程原理

Q5:为什么 condition_.wait(lock, [this]{ return !tasks_.empty() || stop_; }); 要有谓词?

  • 因为可能发生 虚假唤醒 或竞争唤醒。

  • 如果不用谓词,线程可能在 tasks_ 仍然为空时就被唤醒,然后执行 task = tasks_.front() → 直接越界崩溃。

  • 谓词 !tasks_.empty() || stop_ 确保只有在队列里有任务,或者收到停止信号时,线程才会继续。


Q6:为什么在锁外执行 task()

task = std::move(tasks_.front());
tasks_.pop();
} // 这里锁释放
task(); // 在锁外执行
  • 因为 task() 可能是一个耗时任务,如果在持有 queue_mutex_ 时执行,就会阻塞其他线程从 tasks_ 取任务。

  • 正确做法:取出任务后立即释放 queue_mutex_,让其他线程也能并发消费队列。


Q7:析构函数里为什么要先 stop_ = truecondition_.notify_all()

  • 析构函数里加锁修改 stop_

    {std::unique_lock<std::mutex> lock(queue_mutex_);stop_ = true;
    }
    condition_.notify_all();
    
  • 必须先改 stop_,再通知所有等待在 condition_ 上的线程。

  • 否则线程被唤醒时看不到最新的 stop_ 值,会继续 wait,导致死锁。


Q8:为什么 stop_ 不需要是 std::atomic<bool>

  • 因为所有对 stop_ 的访问都在 queue_mutex_ 锁保护下。

  • 互斥锁已经保证了内存可见性和顺序一致性,所以没必要额外用 atomic


三、线程与任务模型

Q9:线程和 CPU 的关系是什么?

  • 在代码里,workers_ 存的是一组 std::thread 对象,每个线程需要被操作系统调度到 CPU 核心上才能运行。

  • 如果线程数 ≤ CPU 核心数 → 真并行;

  • 如果线程数 > CPU 核心数 → 操作系统用时间片轮转,让 workers_ 中的线程轮流在 CPU 上跑。


Q10:CPU 密集 vs IO 密集,在 ThreadPool 参数选择上有什么区别?

  • CPU 密集型任务:比如 task() 里做矩阵运算,CPU 总是满载,线程池大小 ≈ CPU 核心数。

  • IO 密集型任务:比如 task() 里做网络请求,CPU 大部分时间在等 IO → 可以设置 workers_ 数量为 CPU 核心数的 2~4 倍,提高 CPU 利用率。


四、扩展设计

Q11:现在的 submit(std::function<void()> task) 只能提交 void() 任务,如何支持返回值?

  • 可以改为用 std::packaged_task<T()> 封装任务,把 future 返回给调用者。

  • 工作线程依旧在 workers_ 中执行 (*task)();,结果会写入 future 的共享状态。


Q12:如果 task() 抛异常会怎样?

  • 如果 task() 里抛出未捕获的异常,该工作线程会调用 std::terminate(),导致整个程序崩溃。

  • 解决方法:在 task(); 外层包一层 try-catch,把异常捕获并记录下来,保证线程池不会因为一个任务挂掉。


五、面试总结套路(带参数名)

当面试官问你“解释一下这个线程池”时,你可以这样回答:

  1. 整体思路

    • workers_ 里预先创建了 N 个线程,它们在循环里等待任务。

    • 任务被压入 tasks_ 队列,condition_ 唤醒等待的线程。

    • 线程取任务时持有 queue_mutex_,保证对队列的访问安全。

    • 析构时设置 stop_ = true,通知所有线程退出并 join。

  2. 关键细节

    • std::function<void()> 存放任意任务。

    • std::unique_lock + condition_.wait() 防止虚假唤醒。

    • 任务取出后在锁外执行,避免阻塞其他线程。

    • 析构时先改 stop_notify_all(),避免死锁。

  3. 扩展点

    • 返回值支持 → std::packaged_task + std::future

    • 优先级任务 → std::priority_queue

    • 过载保护 → 限制 tasks_ 大小,丢弃或阻塞提交。

http://www.xdnf.cn/news/19329.html

相关文章:

  • 从零开始写个deer-flow-mvp-第一天
  • 拆分TypeScript项目的学习收获:处理编译缓存和包缓存,引用本地项目,使用相对路径
  • 粗糙表面接触模型MATLAB代码
  • 多租户配额与预算:限额、配额周期与突发桶的结算模型(Final)
  • 【机械故障】使用扭矩计算物体重量
  • web墨卡托的纬度范围为什么是85°S~85°N?
  • 为何重定义库函数会减少flash体积(从prinf讲解)
  • 为什么计算机使用补码存储整数:补码的本质
  • 【秋招笔试】2025.08.29阿里云秋招笔试题
  • 【Linux】动静态库的制作与原理
  • 第三十二天:数组
  • 刷算法题-数组-02
  • 关于Ctrl+a不能全选的问题
  • Wi-Fi技术——OSI模型
  • VS安装 .NETFramework,Version=v4.6.x
  • React Hooks useMemo
  • [强网杯2019]随便注-----堆叠注入,预编译
  • centos7挂载iscis存储操作记录
  • postman 用于接口测试,举例
  • postman带Token测试接口
  • DAY50打卡
  • Redis 持久化 AOF 与 RDB 的区别
  • Ruoyi-vue-plus-5.x第二篇MyBatis-Plus数据持久层技术:2.1 MyBatis-Plus核心功能
  • audioLDM模型代码阅读(五)—— pipeline
  • Python学习大集合:基础与进阶、项目实践、系统与工具、Web 开发、测试与运维、人工智能(视频教程)
  • 电力电子技术知识学习-----晶闸管
  • VSCode中使用Markdown
  • 从零开始学炒股
  • cordova+umi 创建项目android APP
  • PythonDay42