线程池常见面试问答
好嘞 👍,我帮你把这些 线程池 + 并发编程八股文 整理成 问答对照表(Q & A),你面试时可以直接用。
🧾 线程池常见面试问答
一、基础语法 & STL
Q1:std::function<void()>
和函数指针的区别?
std::function
是通用的可调用包装器,可以存储 lambda、函数、仿函数、函数指针。函数指针只能存一个函数地址,不支持捕获环境。
std::function
内部通过 类型擦除(type erasure) 实现,灵活但有一定性能开销(可能涉及动态分配)。
Q2:为什么要 std::move(tasks_.front())
?
避免拷贝,直接把任务对象所有权转移到局部变量
task
。如果不用
move
,则会调用拷贝构造,多一次内存分配/复制。不能返回引用,因为
queue.pop()
后元素生命周期结束。
Q3:emplace_back
和 push_back
的区别?
push_back
需要先构造对象再拷贝/移动到容器。emplace_back
在容器内部 原地构造,省一次拷贝/移动。这里
emplace_back([this]{...})
直接在vector
里构造线程对象。
Q4:Lambda 捕获 [this]
有什么风险?
[this]
捕获当前对象指针,如果对象先析构而线程还在运行,就会访问悬空指针 → 未定义行为。避免方式:
在析构函数里设置
stop_ = true;
并join()
所有线程。或者用
shared_from_this
配合智能指针,保证生命周期。
Q5:unique_lock
和 lock_guard
的区别?
lock_guard
:轻量级,构造时加锁,析构时解锁,不可解锁/再锁。unique_lock
:更灵活,可以手动 unlock()/lock(),支持condition_variable::wait()
。所以线程池里必须用
unique_lock
,否则wait()
无法自动释放锁。
二、并发编程原理
Q6:为什么 condition_variable::wait()
需要谓词?
防止 虚假唤醒(spurious wakeup)。
wait(lock, pred)
会反复检查pred
,直到返回 true。否则可能在任务队列仍为空时提前唤醒,导致线程取不到任务。
Q7:为什么 stop_
不用 atomic<bool>
?
因为所有对
stop_
的读写都在mutex
锁保护下,互斥锁已经保证内存可见性。如果去掉锁,仅依赖原子操作也能工作,但需要小心任务队列的并发访问。
Q8:为什么要在锁外执行 task()
?
避免任务执行期间长时间持锁,阻塞其他线程取任务。
正确做法:取任务时加锁,执行任务时释放锁。
Q9:析构函数里为什么先 stop_ = true
再 notify_all()
?
如果先通知,线程被唤醒时可能还看不到
stop_
改变,导致重新进入等待,形成死锁。必须先修改标志,再通知,让线程看到正确的退出条件。
Q10:如果任务抛出异常会怎样?
默认情况下,线程函数中未捕获的异常会调用
std::terminate()
,整个程序崩溃。改进方法:在任务执行时用 try-catch 包裹,保证异常被吞掉或记录。
三、线程池设计扩展
Q11:如何让 submit
支持返回值?
用
std::packaged_task<T()>
包装任务,返回std::future<T>
。提交时存放 packaged_task 到队列,执行时调用它,主线程可通过 future 拿结果。
Q12:线程池关闭时,任务会怎样?
当前设计是 等所有已提交任务执行完再退出。
如果想要立即关闭并丢弃剩余任务,可以:
在析构里清空队列。
或在
submit()
里检测stop_
,拒绝新任务。
Q13:线程数怎么选择?
std::thread::hardware_concurrency()
:返回 CPU 核心数。CPU 密集型任务:线程数 ≈ 核心数。
IO 密集型任务:线程数 > 核心数,考虑
2*N
或更多。
Q14:如何支持任务优先级?
把
std::queue
换成std::priority_queue
,根据任务优先级排序。也可以设计多个队列,高优先级任务先取。
Q15:线程池的任务队列属于什么模型?
本质是 生产者-消费者模型:
submit()
= 生产者,往队列塞任务。工作线程 = 消费者,从队列取任务执行。
如果不加锁,会出现数据竞争(多线程同时修改队列导致未定义行为)。
四、开放性 / 加分题
Q16:stop_
用 volatile
行不行?
不行。
volatile
只保证编译器不优化读写,但不保证原子性和可见性。必须用
atomic
或mutex
才能保证线程安全。
Q17:什么是工作窃取(work stealing)线程池?
普通线程池:所有线程从一个全局队列取任务。
工作窃取:每个线程有自己的任务队列,如果自己队列空,就去“偷”别的线程的任务。
优点:减少竞争,提高负载均衡。
Q18:线程池和协程的区别?
线程池:真正的 OS 线程调度,切换开销大(上下文切换)。
协程:用户态调度,切换只在用户空间进行,开销更小。
协程适合大量 IO 密集型任务;线程池适合 CPU 密集型并行任务。
Q19:无锁队列能提升性能吗?
可以,减少锁竞争。
但需要处理 ABA 问题(两个线程同时修改链表节点)。
解决方法:使用带版本号的 CAS 或 Hazard Pointers。
Q20:如果提交任务太多,队列会爆吗?
是的,任务队列可能无限增长,耗尽内存。
解决办法:
设置队列最大长度,超出时阻塞/拒绝新任务。
或者实现丢弃策略(丢弃最旧/最新任务)。
✅ 总结口诀:
语法:
function/move/lambda/unique_lock
并发:
虚假唤醒/锁外执行/内存可见性/异常安全
扩展:
future/packaged_task/优先级/关闭策略
高阶:
work stealing/协程/无锁队列
要不要我把这一套整理成 Markdown 表格格式,每个问题和答案一行,方便你直接打印/背诵?