线程池:设计原理、实现与任务调度
线程池这东西,说白了就是「提前招一批工人等着干活」。
我刚开始做并发编程时,总觉得来一个任务就 new 一个线程挺爽的。直到有一次线上服务被流量冲垮——每秒几千个请求,每个请求都创建线程,系统直接 OOM 了。嗯,从那以后,线程池就成了我工具箱里的标配。
为什么需要线程池?
你想想看,线程的创建和销毁是有代价的:
- 创建线程需要分配栈空间、初始化内核资源
- 销毁线程需要回收资源、通知内核
- 频繁创建销毁会导致频繁的系统调用
线程池的核心思想就是:复用。提前创建好一批线程,任务来了直接丢给它们处理,处理完线程不销毁,继续等下一个任务。
一句话总结:线程池 = 预先创建的线程集合 + 任务队列 + 调度策略
线程池的设计原理
我习惯把线程池拆成三个核心组件来看:
- 线程管理者:负责线程的创建、销毁、扩容、缩容
- 任务队列:存放待执行的任务,通常是线程安全的队列
- 调度策略:决定任务如何分配给线程,以及线程池如何调整大小
这里我画了一张图,帮你理清它们之间的关系:
线程池的实现
在 C 语言里实现一个线程池,我通常会用到 pthread 库。下面是一个简化版的实现思路:
// 线程池结构体
typedef struct {
pthread_t *threads; // 线程数组
task_queue_t *queue; // 任务队列
int thread_count; // 线程数量
int shutdown; // 关闭标志
pthread_mutex_t lock; // 互斥锁
pthread_cond_t notify; // 条件变量
} thread_pool_t;
// 工作线程函数
void *worker(void *arg) {
thread_pool_t *pool = (thread_pool_t *)arg;
while (1) {
pthread_mutex_lock(&pool->lock);
// 队列为空时等待
while (pool->queue->count == 0 && !pool->shutdown) {
pthread_cond_wait(&pool->notify, &pool->lock);
}
if (pool->shutdown) {
pthread_mutex_unlock(&pool->lock);
pthread_exit(NULL);
}
// 取出任务
task_t task = dequeue(pool->queue);
pthread_mutex_unlock(&pool->lock);
// 执行任务
task.function(task.arg);
}
return NULL;
}
我的经验:条件变量这里容易踩坑。一定要用 while 循环检查条件,而不是 if。因为 pthread_cond_wait 可能被「虚假唤醒」——这是 POSIX 标准允许的。我曾经因为这个 bug 排查了整整两天。
线程池的任务调度
任务调度是线程池的灵魂。我见过几种常见的调度策略:
| 策略名称 | 原理 | 适用场景 |
|---|---|---|
| FIFO(先进先出) | 任务按到达顺序执行 | 通用场景,公平性好 |
| 优先级调度 | 高优先级任务先执行 | 实时系统、关键任务优先 |
| 工作窃取 | 空闲线程从其他线程队列偷任务 | 负载不均衡的场景 |
| 动态扩容 | 根据队列长度动态增减线程数 | 流量波动大的服务 |
我个人最常用的是「动态扩容 + FIFO」的组合。为什么呢?因为大多数业务场景下,任务的执行时间差异不大,FIFO 足够简单高效。动态扩容则能应对突发流量。
避坑指南
我曾经犯过的错:
- 线程数设得太大:以为线程越多越快,结果上下文切换开销把 CPU 吃满了。经验值:CPU 密集型任务,线程数 = CPU 核心数 + 1;IO 密集型任务,可以适当多些。
- 任务队列无界:任务积压太多,内存爆了。一定要用有界队列,配合拒绝策略。
- 忘记处理 shutdown:程序退出时线程池没正确关闭,导致资源泄漏。
一个完整的调度示例
下面是一个带动态扩容的调度逻辑片段:
void thread_pool_schedule(thread_pool_t *pool, task_t task) {
pthread_mutex_lock(&pool->lock);
// 队列满了,考虑扩容
if (queue_is_full(pool->queue)) {
if (pool->thread_count < pool->max_threads) {
// 创建一个新线程
pthread_t new_thread;
pthread_create(&new_thread, NULL, worker, pool);
pool->threads[pool->thread_count++] = new_thread;
} else {
// 执行拒绝策略
reject_task(task);
pthread_mutex_unlock(&pool->lock);
return;
}
}
enqueue(pool->queue, task);
pthread_cond_signal(&pool->notify);
pthread_mutex_unlock(&pool->lock);
}
核心要点:扩容和缩容都要有阈值。我一般设两个阈值:队列长度超过 80% 时扩容,低于 20% 时缩容。避免频繁抖动。
总结一下
线程池不是什么高深的技术,但用好了能省很多事。记住三个关键点:
- 线程数要合理:不是越多越好,要根据任务类型调整
- 队列要有界:防止内存被撑爆
- 调度策略要匹配业务:没有银弹,适合的才是最好的
嗯,线程池的内容就聊到这里。实际项目中,你还会遇到线程池的监控、动态配置、优雅关闭等问题,这些都是在实践中慢慢积累的。