1. 线程池基础与设计思路
在Linux系统编程中,线程池是一种常见的并发编程模式。它通过预先创建一组线程并复用它们来处理多个任务,避免了频繁创建和销毁线程的开销。我最近在Ubuntu 20.04环境下用C语言实现了三种不同风格的线程池,下面分享一些关键实现细节和性能对比。
线程池的核心组件通常包括:
- 任务队列:存放待执行的任务
- 工作线程:执行任务的线程
- 管理线程:负责线程的动态创建和回收
- 同步机制:保证线程安全的数据访问
我设计的这个线程池有几个特点:
- 动态扩容:当任务队列满时自动创建临时线程
- 临时线程回收:空闲超时的临时线程会自动销毁
- 优雅关闭:销毁时能安全回收所有资源
2. 三种实现方案对比
2.1 实现1:定时扫描管理
这个版本的管理线程采用定时唤醒机制,每秒钟检查一次线程池状态:
c复制void *manage_thread_fn(void *arg) {
struct pthread_pool *tp = (struct pthread_pool *)arg;
struct timespec ts;
for (;;) {
pthread_cleanup_push(mutex_unlock, &tp->mutex);
pthread_mutex_lock(&tp->mutex);
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 1;
while (!tp->is_destroy) {
int ret = pthread_cond_timedwait(&tp->m_cond, &tp->mutex, &ts);
if (ret == ETIMEDOUT) break;
}
// ...检查并创建线程...
}
}
优点:
- 实现简单直接
- 性能最好(处理2亿任务耗时36秒)
缺点:
- 管理线程定时唤醒不够及时
- 资源利用率可能不够高效
2.2 实现2:条件触发管理
这个版本改进了管理线程的唤醒机制,只在需要时才唤醒:
c复制int enqueue(struct task *task, struct pthread_pool *tp) {
// ...入队逻辑...
if ((tp->core_thread_num < CORE_THREAD_NUM) ||
(tp->tq->num == MAX_TASK_QUEUE_NUM && tp->tmp_thread_num < TMP_THREAD_NUM)) {
pthread_cond_signal(&tp->m_cond);
}
// ...
}
临时线程增加了超时回收机制:
c复制void *tmp_thread_fn(void *arg) {
// ...
while (!tp->is_destroy && tp->tq->num == 0) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 2;
if (pthread_cond_timedwait(&tp->cond, &tp->mutex, &ts) == ETIMEDOUT) {
// 超时回收
tp->thread_state[idx] = 0;
tp->tmp_thread_num--;
pthread_cond_signal(&tp->m_cond);
pthread_exit(NULL);
}
}
// ...
}
优点:
- 资源管理更精细
- 临时线程能自动回收
- 性能接近实现1(38秒)
缺点:
- 代码复杂度稍高
2.3 实现3:双锁机制
这个版本引入了第二个互斥锁专门保护管理线程相关操作:
c复制struct pthread_pool {
// ...
pthread_mutex_t m_mutex; // 新增的管理锁
// ...
};
int enqueue(struct task *task, struct pthread_pool *tp) {
// ...
pthread_mutex_lock(&tp->m_mutex);
if ((tp->core_thread_num < CORE_THREAD_NUM) ||
(tp->tq->num == MAX_TASK_QUEUE_NUM && tp->tmp_thread_num < TMP_THREAD_NUM)) {
pthread_cond_signal(&tp->m_cond);
}
pthread_mutex_unlock(&tp->m_mutex);
// ...
}
优点:
- 锁粒度更细,理论上并发度更高
- 管理线程和工作线程的锁分离
缺点:
- 加锁开销大导致性能下降明显(67秒)
- 实现复杂度最高
3. 关键实现细节
3.1 任务队列设计
环形缓冲区实现的任务队列:
c复制struct task_queue {
int num; // 当前任务数
struct task *tasks[MAX_TASK_QUEUE_NUM];
int head; // 队头指针
int tail; // 队尾指针
};
int enqueue(struct task *task, struct pthread_pool *tp) {
if (tp->tq->num == MAX_TASK_QUEUE_NUM) {
return ERR;
}
tp->tq->tasks[tp->tq->tail] = task;
tp->tq->tail = (tp->tq->tail + 1) % MAX_TASK_QUEUE_NUM;
tp->tq->num++;
pthread_cond_signal(&tp->cond);
return SUCC;
}
3.2 线程安全退出
使用pthread_cleanup_push确保锁能被正确释放:
c复制void *core_thread_fn(void *arg) {
// ...
for (;;) {
pthread_cleanup_push(mutex_unlock, &tp->mutex);
pthread_mutex_lock(&tp->mutex);
while (tp->tq->num == 0 && !tp->is_destroy) {
pthread_cond_wait(&tp->cond, &tp->mutex);
}
if (tp->is_destroy) {
tp->thread_state[idx] = 0;
pthread_exit(NULL);
}
// ...
pthread_cleanup_pop(0);
}
// ...
}
3.3 优雅关闭实现
销毁时先通知所有线程退出:
c复制int pthread_pool_destroy(struct pthread_pool *tp) {
pthread_mutex_lock(&tp->mutex);
tp->is_destroy = 1;
pthread_cond_broadcast(&tp->cond);
pthread_cond_signal(&tp->m_cond);
pthread_mutex_unlock(&tp->mutex);
// 先取消管理线程
pthread_join(tp->mt, NULL);
// 再取消工作线程
for (int i = 0; i < MAX_THREAD_NUM; i++) {
if (tp->thread_state[i]) {
pthread_cancel(tp->thread[i]);
if (i < CORE_THREAD_NUM) {
pthread_join(tp->thread[i], NULL);
}
}
}
// ...
}
4. 性能优化建议
- 任务批量入队:主线程可以一次放入多个任务,减少锁竞争
c复制for(int i = 0; i < task_num; ) {
pthread_mutex_lock(&tp->mutex);
int available = MAX_TASK_QUEUE_NUM - tp->tq->num;
while (available > 0) {
enqueue(&tasks[i], tp);
i++;
available--;
}
pthread_mutex_unlock(&tp->mutex);
}
-
动态调整核心线程数:根据系统负载自动调整核心线程数量
-
任务窃取机制:当某个线程的任务队列为空时,可以从其他线程的任务队列中"窃取"任务执行
-
避免虚假唤醒:所有条件等待都应该放在while循环中检查条件
c复制while (tp->tq->num == 0 && !tp->is_destroy) {
pthread_cond_wait(&tp->cond, &tp->mutex);
}
5. 常见问题排查
- 线程无法正常退出
- 检查所有可能阻塞的地方是否都检查了is_destroy标志
- 确保正确使用了pthread_cleanup_push/pop
- 性能不如预期
- 检查锁竞争情况,可以用perf工具分析
- 尝试增大任务队列大小
- 考虑使用无锁队列实现
- 内存泄漏
- 确保所有malloc都有对应的free
- 使用valgrind检查内存问题
- 死锁问题
- 确保锁的获取和释放顺序一致
- 避免在持有锁的情况下调用可能阻塞的函数
6. 扩展思考
-
C++11实现:可以使用std::thread、std::mutex等现代C++特性简化实现
-
协程支持:结合协程实现更轻量级的任务调度
-
优先级队列:支持带优先级的任务调度
-
跨平台适配:封装平台相关代码,支持Windows和Linux
在实际项目中,我最终选择了实现2作为基础版本,因为它在性能和代码复杂度之间取得了较好的平衡。对于特别注重性能的场景,可以在实现2的基础上进一步优化锁的使用和任务调度策略。
