一、为什么需要线程池
如果没有线程池,每来一个任务就创建一个线程:
std::thread t(task);
t.detach();
任务少的时候没有明显问题。
但如果短时间内来了大量任务:
任务1
任务2
任务3
任务4
...
任务10000
程序就可能不断:
创建线程
↓
执行任务
↓
销毁线程
线程的创建和销毁本身是有开销的。
而且线程数量如果不受控制,还可能出现:
线程数量过多
↓
频繁上下文切换
↓
CPU调度开销增加
↓
程序性能下降
线程池的思路就是:
程序启动
↓
提前创建固定数量线程
↓
线程一直等待任务
↓
任务来了以后放入队列
↓
空闲线程取任务执行
↓
执行完继续等待
例如提前创建:
4个工作线程
后面来了:
100个任务
也不会创建 100 个线程。
而是:
Task Queue
Task1 → Task2 → Task3 → Task4 → ...
↓ ↓ ↓ ↓
Thread1 Thread2 Thread3 Thread4
四个线程不断从任务队列中取任务执行。
所以线程池主要解决的是:
线程重复利用
+
控制线程数量
+
统一管理任务
从面试角度来看,一个简化线程池最少需要四个核心部分:
工作线程 workers
任务队列 tasks
互斥锁 mutex
条件变量 condition_variable
再加一个:
停止标志 stop
用来控制线程池退出。
二、先设计一个最简单的线程池
先定义线程池:
#include <condition_variable>
#include <functional>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
class ThreadPool {
private:
std::vector<std::thread> workers_; // 工作线程
std::queue<std::function<void()>> tasks_; // 任务队列
std::mutex mutex_; // 保护任务队列
std::condition_variable cv_; // 用于线程等待和唤醒
bool stop_; // 是否停止线程池
public:
explicit ThreadPool(size_t threadCount);
~ThreadPool();
void post(std::function<void()> task);
};
这里最关键的是:
std::vector<std::thread> workers_;
负责保存所有工作线程。
例如:
workers_[0]
workers_[1]
workers_[2]
workers_[3]
分别对应四个线程。
任务则使用:
std::queue<std::function<void()>> tasks_;
保存。
为什么任务类型使用:
std::function<void()>
因为这样可以把各种:
普通函数
Lambda
成员函数包装结果
统一成:
无参数、无返回值的可调用对象
例如:
pool.post([]() {
std::cout << "hello thread pool" << std::endl;
});
这个 Lambda 就可以直接放入任务队列。
所以整个线程池的数据关系可以先理解成:
ThreadPool
│
┌──────────┴──────────┐
↓ ↓
Task Queue Workers
↓ ↓
task1 thread1
task2 thread2
task3 thread3
task4 thread4
工作线程只负责:
不断从Task Queue里面取任务
三、构造函数:创建工作线程并等待任务
线程池构造时,需要创建指定数量的线程。
例如:
ThreadPool pool(4);
表示创建:
4个工作线程
构造函数可以这样实现:
ThreadPool::ThreadPool(size_t threadCount) : stop_(false) {
for (size_t i = 0; i < threadCount; ++i) {
workers_.emplace_back([this]() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this]() {
return stop_ || !tasks_.empty();
});
if (stop_ && tasks_.empty()) return;
task = std::move(tasks_.front());
tasks_.pop();
}
task();
}
});
}
}
这段代码就是线程池最核心的部分。
首先:
workers_.emplace_back(...);
创建一个新线程并保存到:
workers_
里面。
线程运行以后进入:
while (true)
表示工作线程一直存在,不会执行一次任务就退出。
接下来:
std::unique_lock<std::mutex> lock(mutex_);
给任务队列加锁。
因为后面多个线程都可能访问:
tasks_
如果不加锁,就可能出现多个线程同时:
读取tasks_.front()
删除tasks_.front()
添加任务
从而造成数据竞争。
然后进入:
cv_.wait(lock, [this]() {
return stop_ || !tasks_.empty();
});
这一句非常重要。
它表示:
如果现在没有任务
↓
线程进入等待
↓
不一直占用CPU
直到出现:
有新任务
或者
线程池准备退出
才会被唤醒。
所以工作线程不是这样:
while (true) {
if (!tasks_.empty()) {
...
}
}
因为这种写法会一直循环检查:
有没有任务?
有没有任务?
有没有任务?
造成:
CPU空转
而条件变量可以让线程:
没有任务 → 睡眠
有任务 → 唤醒
当线程被唤醒以后:
if (stop_ && tasks_.empty()) return;
表示:
线程池已经停止
+
任务也全部执行完了
那么这个工作线程就可以退出。
如果还有任务:
task = std::move(tasks_.front());
tasks_.pop();
从队头取出一个任务。
然后离开:
锁作用域
最后:
task();
执行任务。
为什么 task() 要放在锁外面?
因为如果写成:
锁住mutex
↓
取任务
↓
执行task
↓
任务执行完
↓
解锁
假设一个任务执行:
5秒
那么这 5 秒里面其他线程都拿不到这个锁。
结果就是:
虽然有多个工作线程
实际上一次只能执行一个任务
所以正确流程应该是:
加锁
↓
从队列取任务
↓
删除任务
↓
解锁
↓
真正执行任务
锁只负责保护:
任务队列
而不是保护任务本身的执行。
四、Post任务和线程池如何优雅退出
有了工作线程以后,还需要向线程池提交任务。
可以写一个:
void ThreadPool::post(std::function<void()> task) {
{
std::lock_guard<std::mutex> lock(mutex_);
if (stop_) return;
tasks_.push(std::move(task));
}
cv_.notify_one();
}
提交一个任务:
pool.post([]() {
std::cout << "task running" << std::endl;
});
整个过程就是:
调用post
↓
加锁
↓
任务push进queue
↓
解锁
↓
notify_one
↓
唤醒一个工作线程
↓
线程取出任务
↓
执行task()
这里:
cv_.notify_one();
为什么只唤醒一个线程?
因为:
只加入了一个新任务
所以通常只需要唤醒:
一个等待线程
即可。
如果一次加入了很多任务,也可以根据设计选择:
notify_all();
但是简单线程池一般:
notify_one();
就够了。
接下来是线程池中非常重要的一个问题:
析构的时候怎么安全退出?
不能直接让对象析构。
因为此时:
workers_里面的线程
可能还在运行。
正确流程应该是:
设置stop = true
↓
唤醒所有等待线程
↓
线程把剩余任务执行完
↓
所有线程退出
↓
主线程join
↓
线程池正式销毁
析构函数可以这样写:
ThreadPool::~ThreadPool() {
{
std::lock_guard<std::mutex> lock(mutex_);
stop_ = true;
}
cv_.notify_all();
for (std::thread &worker : workers_) {
if (worker.joinable()) worker.join();
}
}
首先:
stop_ = true;
告诉所有工作线程:
线程池准备关闭
然后:
cv_.notify_all();
为什么这里是:
notify_all
而不是:
notify_one
因为可能有:
多个工作线程正在wait
例如:
Thread1 → wait
Thread2 → wait
Thread3 → wait
Thread4 → wait
线程池要关闭时,需要把:
所有线程
都唤醒。
线程醒来以后判断:
if (stop_ && tasks_.empty()) return;
如果队列里还有任务:
继续执行
如果任务已经全部执行完成:
退出线程函数
最后析构函数:
worker.join();
等待工作线程真正退出。
这就是所谓的:
优雅关闭
而不是任务执行到一半就强行结束线程。
完整代码如下:
#include <condition_variable>
#include <functional>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
class ThreadPool {
private:
std::vector<std::thread> workers_;
std::queue<std::function<void()>> tasks_;
std::mutex mutex_;
std::condition_variable cv_;
bool stop_;
public:
explicit ThreadPool(size_t threadCount) : stop_(false) {
for (size_t i = 0; i < threadCount; ++i) {
workers_.emplace_back([this]() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this]() {
return stop_ || !tasks_.empty();
});
// 停止并且已经没有任务,线程退出
if (stop_ && tasks_.empty()) return;
// 取出一个任务
task = std::move(tasks_.front());
tasks_.pop();
}
// 在锁外执行任务
task();
}
});
}
}
~ThreadPool() {
{
std::lock_guard<std::mutex> lock(mutex_);
stop_ = true;
}
// 唤醒所有工作线程
cv_.notify_all();
// 等待所有线程退出
for (std::thread &worker : workers_) {
if (worker.joinable()) worker.join();
}
}
void post(std::function<void()> task) {
{
std::lock_guard<std::mutex> lock(mutex_);
if (stop_) return;
tasks_.push(std::move(task));
}
// 新增一个任务,唤醒一个线程即可
cv_.notify_one();
}
};
int main() {
ThreadPool pool(4);
for (int i = 0; i < 10; ++i) {
pool.post([i]() {
std::cout << "task " << i << " thread = "
<< std::this_thread::get_id() << std::endl;
});
}
return 0;
}
整个线程池运行过程可以整理成:
创建ThreadPool
↓
创建N个Worker
↓
Worker进入wait
↓
post(task)
↓
task进入queue
↓
notify_one
↓
Worker被唤醒
↓
从queue取任务
↓
解锁
↓
执行task
↓
再次进入wait
析构时:
ThreadPool析构
↓
stop = true
↓
notify_all
↓
所有Worker被唤醒
↓
继续执行剩余任务
↓
queue为空
↓
Worker return
↓
join所有线程
↓
线程池销毁
五、面试最容易继续追问什么
把上面的线程池写出来以后,面试官通常不会马上结束,而是会继续围绕几个细节追问。
1. 为什么需要condition_variable?
因为如果没有条件变量,很容易写成:
while (true) {
if (!tasks_.empty()) {
// 执行任务
}
}
没有任务的时候线程仍然一直运行。
这叫:
忙等待
会不断消耗 CPU。
使用:
cv_.wait();
以后:
没有任务
↓
线程睡眠
有任务
↓
notify
↓
线程唤醒
这样更加合理。
2. 为什么wait必须配合unique_lock?
常见写法是:
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, predicate);
因为 wait() 在等待时需要:
自动释放mutex
让其他线程能够:
post任务
被唤醒以后又需要:
重新获得mutex
再继续访问任务队列。
也就是:
wait
↓
自动unlock
↓
线程睡眠
↓
notify
↓
重新lock
↓
继续执行
unique_lock 支持这种:
动态加锁和解锁
所以条件变量通常和:
std::unique_lock
一起使用。
3. 为什么wait还需要判断条件?
也就是:
cv_.wait(lock, [this]() {
return stop_ || !tasks_.empty();
});
而不是简单:
cv_.wait(lock);
一个重要原因是需要防止:
虚假唤醒
线程可能在没有真正新任务的情况下从 wait() 返回。
所以醒来以后仍然必须重新确认:
到底有没有任务?
是不是准备停止?
使用带谓词版本相当于帮我们做:
while (!condition) {
cv_.wait(lock);
}
4. 为什么task要在锁外执行?
因为锁真正保护的是:
tasks_
如果:
task();
也放在锁里面:
线程A拿到锁
↓
执行一个10秒任务
↓
10秒后释放锁
其他工作线程这 10 秒都无法取任务。
线程池就失去了:
并发执行
的意义。
所以:
锁内:只取任务
锁外:执行任务
这是非常重要的一点。
5. 为什么析构一定要join?
如果:
std::thread
对象析构时仍然:
joinable
程序可能直接:
std::terminate
所以线程池销毁之前必须处理这些线程。
这里选择:
join()
表示:
主线程等待工作线程结束
保证线程生命周期完整结束。
6. stop为什么也要在锁里面修改?
因为:
stop_
不仅析构线程会访问。
工作线程也会读取:
stop_ || !tasks_.empty()
如果多个线程同时访问并且没有同步,就可能产生:
数据竞争
所以这里让:
stop_
tasks_
都由同一个:
mutex_
保护。
另一种方案也可以考虑:
std::atomic<bool> stop_;
但对于这个简化实现来说,让状态和任务队列共用锁会更加直观。
7. 这个线程池还可以怎么优化?
当前只是:
面试简化版
真正工程中的线程池还可能继续支持:
动态线程数量
任务返回值
future / packaged_task
任务优先级
有界任务队列
拒绝策略
线程空闲回收
异常处理
任务取消
工作窃取
例如当前:
void post(std::function<void()> task);
没有返回值。
如果希望拿到任务结果,可以进一步结合:
std::future
std::packaged_task
实现:
auto future = pool.submit([]() {
return 10 + 20;
});
std::cout << future.get() << std::endl;
不过面试现场如果让手写一个简单线程池,一般先把:
workers
+
task queue
+
mutex
+
condition_variable
+
stop
+
join
这套核心流程写正确,比一开始追求复杂功能更重要。
面试时可以把整个线程池总结成:
我实现的是一个固定线程数量的简化线程池。构造函数提前创建多个 Worker,每个 Worker 循环从线程安全的任务队列中获取
std::function<void()>类型的任务。任务为空时通过condition_variable阻塞等待,post()添加任务后使用notify_one()唤醒工作线程。任务取出后会先释放任务队列的锁,再真正执行任务,避免长时间占用互斥锁。析构时设置停止标志并notify_all(),让 Worker 执行完剩余任务后退出,最后通过join()等待所有线程安全结束。
如果还能补一句:
核心本质就是"生产者消费者模型"。
基本就已经把这一题的关键点说清楚了。