线程池设计:线程池实现、任务队列、动态调整线程数
线程池这东西,说白了就是「提前招一批工人,有活就干,没活待命」。
我早年做高并发服务时,每个请求都 new 一个线程,结果系统直接崩了。线程创建销毁的开销,比你想象的大得多。后来我才明白——线程池不是锦上添花,而是刚需。
1. 线程池的核心思想
线程池的本质,是一个「生产者-消费者」模型。
- 生产者:提交任务的代码(比如你的主线程)
- 消费者:线程池里的工作线程
- 缓冲区:任务队列
你想想看,如果没有线程池,每个任务来了就创建线程,任务结束就销毁。高并发下,光线程切换就能把 CPU 吃光。我见过一个项目,高峰期每秒创建 5000 个线程,最后系统直接 OOM。
核心原则:线程的创建和销毁,应该由池子统一管理,而不是交给业务代码。
2. 线程池的三大组件
一个完整的线程池,至少需要三样东西:
- 线程数组:存放工作线程的容器
- 任务队列:存放待执行任务的缓冲区
- 管理组件:负责线程的创建、销毁、调度
我个人习惯把线程池设计成「可配置的」——最小线程数、最大线程数、队列容量,都让调用方自己定。这样灵活得多。
3. 任务队列的实现
任务队列,说白了就是一个「先进先出」的链表或数组。但要注意线程安全。
我建议用 pthread_mutex_t 加锁,配合 pthread_cond_t 做条件变量。为什么?因为工作线程需要「等任务」——没任务时休眠,有任务时唤醒。
// 任务节点
typedef struct task_node {
void (*func)(void* arg);
void* arg;
struct task_node* next;
} task_node_t;
// 任务队列
typedef struct task_queue {
task_node_t* head;
task_node_t* tail;
int count;
pthread_mutex_t mutex;
pthread_cond_t cond;
} task_queue_t;
嗯,这里要注意:pthread_cond_wait 必须和 pthread_mutex_lock 配对使用。我曾经见过有人把锁放错位置,结果线程永远醒不过来——那叫一个惨。
4. 线程池的核心结构
typedef struct thread_pool {
pthread_t* threads; // 工作线程数组
int min_threads; // 最小线程数
int max_threads; // 最大线程数
int current_threads; // 当前线程数
int idle_threads; // 空闲线程数
int shutdown; // 关闭标志
task_queue_t* queue; // 任务队列
pthread_mutex_t pool_mutex;
pthread_cond_t pool_cond;
} thread_pool_t;
这里有个细节:idle_threads 这个字段,很多人会忽略。但动态调整线程数时,它就是关键指标。
5. 工作线程的循环逻辑
每个工作线程,其实就是一个死循环:
void* worker_thread(void* arg) {
thread_pool_t* pool = (thread_pool_t*)arg;
while (1) {
pthread_mutex_lock(&pool->pool_mutex);
// 队列为空,且池子没关闭,就休眠
while (pool->queue->count == 0 && !pool->shutdown) {
pool->idle_threads++;
pthread_cond_wait(&pool->pool_cond, &pool->pool_mutex);
pool->idle_threads--;
}
if (pool->shutdown) {
pthread_mutex_unlock(&pool->pool_mutex);
pthread_exit(NULL);
}
// 取任务
task_node_t* task = dequeue(pool->queue);
pthread_mutex_unlock(&pool->pool_mutex);
// 执行任务(注意:这里已经释放了锁)
task->func(task->arg);
free(task);
}
}
你看,逻辑其实不复杂。但有一个坑:执行任务时一定要释放锁。否则其他线程拿不到任务,等于串行执行了。我早期犯过这个错,性能直接打三折。
6. 动态调整线程数
动态调整,说白了就是「根据负载增减线程」。怎么判断负载?看两个指标:
- 任务队列长度:队列越长,说明处理不过来,需要加线程
- 空闲线程数:空闲线程太多,说明资源浪费,需要减线程
我常用的策略是:
| 场景 | 操作 | 触发条件 |
|---|---|---|
| 队列长度 > 阈值 | 增加线程(不超过 max_threads) | 每 1 秒检查一次 |
| 空闲线程 > 阈值 | 减少线程(不低于 min_threads) | 持续空闲超过 3 秒 |
void adjust_threads(thread_pool_t* pool) {
pthread_mutex_lock(&pool->pool_mutex);
int queue_len = pool->queue->count;
int idle = pool->idle_threads;
// 任务积压,加线程
if (queue_len > 10 && pool->current_threads < pool->max_threads) {
int add = min(5, pool->max_threads - pool->current_threads);
for (int i = 0; i < add; i++) {
pthread_t tid;
pthread_create(&tid, NULL, worker_thread, pool);
pool->threads[pool->current_threads++] = tid;
}
printf("[调整] 增加 %d 个线程,当前 %d\n", add, pool->current_threads);
}
// 空闲太多,减线程(通过设置标志,让线程自己退出)
if (idle > 5 && pool->current_threads > pool->min_threads) {
// 这里用条件变量唤醒空闲线程,让它们检查退出标志
// 具体实现略
}
pthread_mutex_unlock(&pool->pool_mutex);
}
嗯,这里要注意:减少线程时,不要直接 pthread_cancel。那样太粗暴,可能导致任务执行一半被中断。我建议用「优雅退出」——设置一个退出标志,让线程执行完当前任务后自己退出。
7. 线程池的关闭
关闭线程池,分两步:
- 设置
shutdown = 1,唤醒所有休眠线程 - 等待所有线程退出(pthread_join)
我曾经见过有人只做第一步,结果主线程退出了,子线程还在跑——成了孤儿线程。嗯,这很危险。
8. 整体架构图
下面这张图,展示了线程池的核心流程:
9. 避坑指南
我曾经踩过的坑:
- 死锁:在任务执行函数里又去提交任务,导致线程池自己等自己。解决方案:任务里不要直接操作线程池。
- 线程泄漏:线程退出时没有 detach 或 join,导致资源无法回收。我建议统一用 join,并在关闭时确保所有线程退出。
- 虚假唤醒:pthread_cond_wait 可能被意外唤醒。所以一定要用 while 循环判断条件,不要用 if。
我的个人建议:
刚开始实现时,先不要搞动态调整。固定线程数跑通,再慢慢加功能。一口吃不成胖子,线程池也一样。
10. 总结
线程池设计,说白了就是三件事:
- 管好任务队列——加锁、条件变量、FIFO
- 管好工作线程——创建、销毁、优雅退出
- 管好动态调整——根据负载增减线程,别拍脑袋
嗯,这些东西说起来简单,但真正写好,需要反复调试。我当年第一个线程池版本,光死锁就修了三天。但一旦跑通,那种成就感,值得。
公众号:蓝海资料掘金营,微信 deep3321