本文整理自学习笔记,涵盖线程池概念、生产者-消费者模型、手撕线程池代码、双队列优化,以及线程数量计算公式。
一、什么是池式结构?
在聊线程池之前,先理解"池"这个概念。
池(Pool) 是一种经典的设计模式,核心思想是资源复用:
| 池的类型 | 复用的是什么 |
|---|---|
| 内存池 | 动态内存分配 |
| 对象池 | 对象的创建与销毁 |
| 连接池 | 数据库 / 网络连接 |
| 请求池 | HTTP 请求对象 |
| 线程池 | 线程的创建与销毁 |
线程为什么需要池?
- 进程是操作系统分配资源的基本单位
- 线程是操作系统进行 CPU 调度的基本单位
- 线程参与 CPU 调度,不使用时进入休眠状态
- 线程创建和销毁是有开销的------每次都 new/destroy 线程,系统负担很大
线程池的本质:预先创建一定数量的线程,用完不销毁,放回池中复用,避免频繁创建销毁带来的开销。
二、线程池解决什么问题?
核心场景
- 异步执行耗时任务------不让主线程(生产者线程)被耗时操作阻塞
- 充分利用多核------在多核 CPU 上并行处理任务
耗时有两种
| 类型 | 举例 | 特点 |
|---|---|---|
| CPU 密集型 | 加密解密、图像处理、数值计算 | 持续占用 CPU,几乎不等待 |
| IO 密集型 | 文件读写、网络请求、数据库查询 | 大部分时间在等待 IO 就绪 |
三、生产者-消费者模型
线程池的骨架是生产者-消费者模型:
┌─────────────────────────────────────────────┐
│ 任务队列(ThreadPool) │
│ ┌──────────────────────────────────────┐ │
│ │ task1 │ task2 │ task3 │ ... │ taskN │ │
│ └──────────────────────────────────────┘ │
│ │
│ 生产者侧 (Producer) 消费者侧 (Consumer)│
│ ────────────── ──────────────── │
│ 主线程 / 工作线程 线程池中的 Worker │
│ Push(task) Pop() → 执行任务 │
└─────────────────────────────────────────────┘
流程:
- 生产者线程 将耗时任务
Push到任务队列,然后继续干自己的事 - 消费者线程 (Worker)不断从队列
Pop任务来执行 - 当队列为空,Worker 线程阻塞等待,不消耗 CPU
- 有新任务时,
notify_one唤醒一个休眠的 Worker
四、为什么用队列而不是栈?
| 队列(Queue) | 栈(Stack) | |
|---|---|---|
| 结构 | 一端进、另一端出 | 一端进、同一端出 |
| 线程场景 | 生产者和消费者各占一端,职责清晰 | 生产者和消费者会抢同一端,容易冲突 |
| 时间复杂度 | 插入/删除都是 O(1) | 插入/删除都是 O(1) |
关键点:队列天然适配生产者-消费者模型------两端职责分离,不需要竞争同一个位置。
五、手撕线程池:基础版
5.1 阻塞队列 BlockingQueue
cpp
#pragma once
#include <condition_variable>
#include <functional>
#include <queue>
#include <mutex>
#include <thread>
template<typename T>
class BlockingQueue {
public:
explicit BlockingQueue(bool nonblock = false)
: nonblock_(nonblock) {}
// 入队
void Push(const T& value) {
std::lock_guard<std::mutex> lock(mutex_); // RAII 自动释放锁
queue_.push(value);
not_empty_.notify_one(); // 唤醒一个等待中的线程
}
// 出队 ------ Pop 返回 true 表示成功,false 表示队列空或已取消
bool Pop(T& value) {
std::unique_lock<std::mutex> lock(mutex_);
// wait 语义:
// 1. unlock(mutex_)
// 2. 线程在 wait 中阻塞,等待 notify
// 3. 被唤醒后重新 lock(mutex_)
// 4. 检查 lambda 条件:队列非空 或 已设置为 nonblock_
not_empty_.wait(lock,
[this] { return !queue_.empty() || nonblock_; });
if (queue_.empty()) return false; // nonblock_ 模式下直接返回
value = queue_.front();
queue_.pop();
return true;
}
// 停止线程池时调用,解除所有阻塞在该队列上的线程
void Cancel() {
std::lock_guard<std::mutex> lock(mutex_);
nonblock_ = true;
not_empty_.notify_all(); // 唤醒全部等待线程
}
private:
bool nonblock_; // true = 非阻塞模式
std::queue<T> queue_; // 任务队列
std::mutex mutex_; // 保护队列的互斥锁
std::condition_variable not_empty_; // 队列非空条件变量
};
几个关键点:
std::lock_guard------ RAII 思想,作用域结束自动解锁std::unique_lock------ 比lock_guard灵活,可以手动 unlock(wait需要先解锁再阻塞)wait+ lambda ------ 解决 spurious wakeup(伪唤醒)问题notify_onevsnotify_all------ 只唤醒一个等待线程,避免惊群效应
5.2 线程池 ThreadPool
cpp
class ThreadPool {
public:
// 禁止隐式转换:ThreadPool tp = 4; 不允许
explicit ThreadPool(int threadNum) {
for (int i = 0; i < threadNum; ++i) {
workers_.emplace_back([this] { Worker(); });
}
}
// 析构:先 Cancel 队列,再等待所有线程安全退出
~ThreadPool() {
task_queue_.Cancel();
for (auto& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
}
// 提交任务
void Post(std::function<void()> task) {
task_queue_.Push(task);
}
private:
void Worker() {
while (true) {
std::function<void()> task;
// Pop 返回 false = 线程池被 Cancel,Worker 退出
if (!task_queue_.Pop(task)) {
break;
}
task(); // 执行任务
}
}
BlockingQueue<std::function<void()>> task_queue_;
std::vector<std::thread> workers_;
};
使用示例:
cpp
int main() {
ThreadPool pool(4); // 4 个 Worker 线程
pool.Post([] { std::cout << "task 1\n"; });
pool.Post([] { std::cout << "task 2\n"; });
std::this_thread::sleep_for(std::chrono::seconds(1));
// 析构时自动等待所有任务完成
return 0;
}
六、手撕线程池:双队列优化
6.1 基础版的问题
在基础版中,所有 Worker 线程和所有生产者线程共用一把锁:
- 生产者
Push时要抢锁 - 消费者
Pop时也要抢锁
当线程数量增多,锁竞争会成为性能瓶颈。
6.2 双队列思路
核心思想:交换队列,让生产者和消费者分别操作不同的队列,减少锁竞争。
时刻 T1:
生产者队列 (prod_queue_): [taskA, taskB, taskC]
消费者队列 (cons_queue_): []
时刻 T2(SwapQueue_ 执行后):
生产者队列 (prod_queue_): []
消费者队列 (cons_queue_): [taskA, taskB, taskC] ← Worker 从这里取
期间:
- 生产者只操作 prod_queue_(持有 prod_mutex_)
- 消费者只操作 cons_queue_(持有 cons_mutex_)
- 两把锁完全独立,井水不犯河水
6.3 实现
cpp
template<typename T>
class BlockingQueuePro {
public:
explicit BlockingQueuePro(bool nonblock = false)
: nonblock_(nonblock) {}
// 生产者侧:只操作 prod_queue_
void Push(const T& value) {
std::lock_guard<std::mutex> lock(prod_mutex_);
prod_queue_.push(value);
not_empty_.notify_one();
}
// 消费者侧:只操作 cons_queue_
bool Pop(T& value) {
std::unique_lock<std::mutex> lock(cons_mutex_);
if (cons_queue_.empty() && SwapQueue_() == 0) {
return false; // 无任务可处理
}
value = cons_queue_.front();
cons_queue_.pop();
return true;
}
void Cancel() {
std::lock_guard<std::mutex> lock(prod_mutex_);
nonblock_ = true;
not_empty_.notify_all();
}
private:
// 原子地交换两个队列
// 只有在 cons_queue_ 为空时才调用
int SwapQueue_() {
std::unique_lock<std::mutex> lock(prod_mutex_);
not_empty_.wait(lock,
[this] { return !prod_queue_.empty() || nonblock_; });
std::swap(prod_queue_, cons_queue_); // O(1) 交换指针
return cons_queue_.size();
}
bool nonblock_;
std::queue<T> prod_queue_; // 生产者往这里写
std::queue<T> cons_queue_; // 消费者从这里读
std::mutex prod_mutex_; // 保护生产者队列
std::mutex cons_mutex_; // 保护消费者队列
std::condition_variable not_empty_;
};
性能提升点:
- 生产者和消费者操作不同的锁,可以并行执行
std::swap是 O(1),只是交换指针,零拷贝- 大部分情况下,锁竞争大幅减少
七、线程数量怎么定?
线程数量和任务类型息息相关:
CPU 密集型
任务全是计算,不涉及 IO 等待。
经验公式:
线程数 = CPU 核心数 + 1
多出来的 1 是为了当某个线程缺页中断时,其他线程还能干活。
IO 密集型
任务大部分时间在等待 IO 就绪,CPU 空闲。
公式:
线程数 = CPU 核心数 × (CPU运算时间 + IO等待时间) / CPU运算时间
通常取 CPU 核心数的 2 倍,因为 IO 等待时间往往远大于 CPU 计算时间。
实际调优
理论值只是起点,真正的最优值需要实测:
- 取不同的线程数(核心数、核心数+1、2倍、4倍...)
- 测量吞吐量(单位时间内完成的任务数)
- 画出曲线,找到拐点
八、完整代码汇总
cpp
#pragma once
#include <condition_variable>
#include <functional>
#include <queue>
#include <mutex>
#include <thread>
// ==================== 基础阻塞队列 ====================
template<typename T>
class BlockingQueue {
public:
explicit BlockingQueue(bool nonblock = false)
: nonblock_(nonblock) {}
void Push(const T& value) {
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(value);
not_empty_.notify_one();
}
bool Pop(T& value) {
std::unique_lock<std::mutex> lock(mutex_);
not_empty_.wait(lock,
[this] { return !queue_.empty() || nonblock_; });
if (queue_.empty()) return false;
value = queue_.front();
queue_.pop();
return true;
}
void Cancel() {
std::lock_guard<std::mutex> lock(mutex_);
nonblock_ = true;
not_empty_.notify_all();
}
private:
bool nonblock_;
std::queue<T> queue_;
std::mutex mutex_;
std::condition_variable not_empty_;
};
// ==================== 双队列优化版 ====================
template<typename T>
class BlockingQueuePro {
public:
explicit BlockingQueuePro(bool nonblock = false)
: nonblock_(nonblock) {}
void Push(const T& value) {
std::lock_guard<std::mutex> lock(prod_mutex_);
prod_queue_.push(value);
not_empty_.notify_one();
}
bool Pop(T& value) {
std::unique_lock<std::mutex> lock(cons_mutex_);
if (cons_queue_.empty() && SwapQueue_() == 0) {
return false;
}
value = cons_queue_.front();
cons_queue_.pop();
return true;
}
void Cancel() {
std::lock_guard<std::mutex> lock(prod_mutex_);
nonblock_ = true;
not_empty_.notify_all();
}
private:
int SwapQueue_() {
std::unique_lock<std::mutex> lock(prod_mutex_);
not_empty_.wait(lock,
[this] { return !prod_queue_.empty() || nonblock_; });
std::swap(prod_queue_, cons_queue_);
return cons_queue_.size();
}
bool nonblock_;
std::queue<T> prod_queue_;
std::queue<T> cons_queue_;
std::mutex prod_mutex_;
std::mutex cons_mutex_;
std::condition_variable not_empty_;
};
// ==================== 线程池 ====================
class ThreadPool {
public:
explicit ThreadPool(int threadNum) {
for (int i = 0; i < threadNum; ++i) {
workers_.emplace_back([this] { Worker(); });
}
}
~ThreadPool() {
task_queue_.Cancel();
for (auto& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
}
void Post(std::function<void()> task) {
task_queue_.Push(task);
}
private:
void Worker() {
while (true) {
std::function<void()> task;
if (!task_queue_.Pop(task)) {
break;
}
task();
}
}
// 换成 BlockingQueuePro 可获得更好的并发性能
BlockingQueue<std::function<void()>> task_queue_;
std::vector<std::thread> workers_;
};
九、总结
┌──────────────────────────────────────────────────────────┐
│ 线程池核心要点 │
├──────────────────────────────────────────────────────────┤
│ 思想:资源复用 + 生产者-消费者模型 │
│ 阻塞队列:用条件变量实现线程间同步 │
│ wait + lambda:防止伪唤醒 │
│ 双队列优化:生产者和消费者各用各的锁,减少竞争 │
│ 线程数:CPU密集型→核心数+1,IO密集型→2×核心数 │
│ 真正的最优值靠实测吞吐量确定 │
└──────────────────────────────────────────────────────────┘