线程池八股文
🧾 线程池八股文
一、基础语法 & 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_ = true
再 condition_.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,把异常捕获并记录下来,保证线程池不会因为一个任务挂掉。
五、面试总结套路(带参数名)
当面试官问你“解释一下这个线程池”时,你可以这样回答:
整体思路:
workers_
里预先创建了 N 个线程,它们在循环里等待任务。任务被压入
tasks_
队列,condition_
唤醒等待的线程。线程取任务时持有
queue_mutex_
,保证对队列的访问安全。析构时设置
stop_ = true
,通知所有线程退出并 join。
关键细节:
用
std::function<void()>
存放任意任务。用
std::unique_lock
+condition_.wait()
防止虚假唤醒。任务取出后在锁外执行,避免阻塞其他线程。
析构时先改
stop_
再notify_all()
,避免死锁。
扩展点:
返回值支持 →
std::packaged_task
+std::future
。优先级任务 →
std::priority_queue
。过载保护 → 限制
tasks_
大小,丢弃或阻塞提交。