线程池
线程池
一、线程池概述
1. 什么是线程池
线程池(Thread Pool)是一种多线程处理形式,它预先创建一定数量的工作线程,并将待处理的任务放入队列中,由这些工作线程循环地从队列中取出任务并执行。线程池属于池式结构(Pooling)的一种,与内存池、连接池等类似,旨在解决资源频繁创建和销毁带来的性能开销问题。
核心思想:通过复用已创建的线程,减少线程创建和销毁的次数,从而提升系统吞吐量和资源利用率。
2. 为什么需要固定数量的线程
线程池中的线程数量通常是固定的,主要原因如下:
- CPU核心数有限:每个CPU核心在同一时刻只能运行一个线程。如果线程数量超过CPU核心数,会导致频繁的上下文切换,增加操作系统调度负担,反而降低性能。
- 性能拐点:随着线程数量增加,系统吞吐量先上升后下降。当线程数超过某个临界值(由任务类型、硬件等决定),性能会因资源竞争而下降。
- 资源管理:固定线程数便于控制资源占用,避免因无限创建线程导致系统资源耗尽(如内存溢出)。
3. 线程池的管理与维持
线程池的管理主要体现在:
- 任务调度:当有新任务提交时,线程池将任务放入队列。如果存在空闲线程,则唤醒它执行任务;否则任务在队列中等待。
- 线程状态控制:通过休眠/唤醒机制动态调整线程的活动状态。队列为空时,线程自动休眠释放CPU;队列非空时,唤醒线程处理任务。
二、线程池的组成部分
一个典型的线程池包含两个核心组件:
- 线程集合(Thread Pool):一组固定数量的工作线程,负责执行任务。
- 任务队列(Task Queue):一个线程安全的队列,用于存储待处理的任务。
1. 线程集合
线程集合的大小(即线程数量)需要根据任务类型合理设置:
- CPU密集型任务:线程数通常设置为CPU核心数(或核心数+1),避免过多线程竞争CPU。
- I/O密集型任务:线程数可以设置得更多(例如
2 * CPU核心数 + 2),因为线程在等待I/O时会主动让出CPU,可以利用这段时间执行其他任务。实际最优值需通过压测确定。
2. 任务队列
任务队列是生产者-消费者模型的缓冲区,需要支持高效、线程安全的入队和出队操作。常用实现方式包括:
- 链表队列:通过头尾指针实现FIFO,入队/出队时间复杂度为O(1)。
- 环形缓冲区:固定大小,适合任务数量可预估的场景。
队列操作需要加锁以保证线程安全。锁的粒度直接影响性能:
| 锁类型 | 特点 | 适用场景 |
|---|---|---|
| 互斥锁(mutex) | 重量级锁,线程阻塞时会进入内核态睡眠 | 临界区操作耗时较长,线程可能长时间阻塞 |
| 自旋锁(spinlock) | 轻量级锁,线程忙等待(不释放CPU) | 临界区操作极短,线程等待时间短,避免上下文切换 |
在线程池中,任务队列的操作通常很快(仅指针操作),因此使用自旋锁可以减少线程切换开销。但需注意自旋锁在单核CPU上无意义(会一直占用CPU)。
三、线程池的工作原理
线程池本质上是一个生产者-消费者模型:
- 生产者线程:负责创建任务并将其放入任务队列。
- 消费者线程:即线程池中的工作线程,循环从队列中取出任务并执行。
工作流程:
- 初始化时,创建固定数量的工作线程,所有线程初始状态为阻塞(等待任务)。
- 当生产者提交任务时,将任务加入队列,并唤醒一个等待的工作线程。
- 工作线程被唤醒后,从队列中取出任务执行,执行完毕后再次尝试取任务:
- 若队列非空,继续取任务执行;
- 若队列为空,线程进入阻塞状态,等待新任务。
- 当线程池销毁时,设置退出标志,并唤醒所有线程,让它们处理完当前任务后退出。
1. 应用场景
线程池广泛应用于以下场景:
- IO密集型操作:如网络请求、磁盘读写,通过线程池并发处理,避免阻塞主线程。
- 任务数量多且耗时:例如Web服务器处理HTTP请求,每个请求独立处理,使用线程池可显著提升吞吐量。
典型例子:
- 单Reactor网络模型(如Redis)中,如果某个连接的数据读写耗时较长,会导致其他连接被阻塞。解决方案是将IO操作封装为任务提交给线程池,实现并行处理。
- Nginx、Memcached等高性能服务器均使用线程池处理IO事件。
- Workflow、Scanner等开源框架中,线程池是异步任务调度的核心组件。
四、手写线程池(C语言实现)
为了深入理解线程池原理,我们将用C语言实现一个简单的线程池,避免依赖C++的高级特性,专注于核心机制。
1. 接口设计
用户只需要知道三个核心接口:
// thread_pool.h
typedef struct thread_pool thread_pool_t;
// 创建线程池,指定工作线程数量
thread_pool_t* thread_pool_create(int thread_count);
// 提交任务:函数指针 + 上下文参数
int thread_pool_post(thread_pool_t* pool, void (*func)(void*), void* arg);
// 销毁线程池
void thread_pool_destroy(thread_pool_t* pool);
设计原则:
- 隐藏内部细节:用户无需知道任务队列、线程管理等内部结构。
- 上下文传递:任务可能需要访问生产者线程的局部数据,通过
void* arg传递,由调用者负责内存管理(通常使用堆内存,任务执行完毕后释放)。
2. 数据结构设计
2.1 任务节点
每个任务被封装成一个节点,节点中包含函数指针和参数,以及一个指向下一个节点的指针(用于构建队列)。
typedef struct task {
struct task* next; // 必须作为第一个成员,便于通用队列操作
void (*func)(void*);
void* arg;
} task_t;
注意:next 指针必须是第一个成员,这样可以通过强制类型转换将任意任务结构视为链表节点,实现通用队列。
2.2 任务队列
队列结构包含头指针、尾指针、锁和条件变量,以及一个阻塞标志(用于控制非阻塞模式)。
typedef struct task_queue {
task_t* head; // 队列头
task_t** tail; // 指向尾节点的next指针的地址(二维指针技巧)
pthread_spinlock_t spinlock; // 保护队列操作的自旋锁
pthread_mutex_t mutex; // 与条件变量配合使用的互斥锁
pthread_cond_t cond; // 条件变量,用于线程阻塞/唤醒
int block; // 阻塞模式标志(1表示阻塞,0表示非阻塞)
} task_queue_t;
二维指针 tail 的巧妙之处:
head指向第一个任务节点。tail指向最后一个节点的next指针的地址(即&(last_node->next))。- 初始时队列为空,
head = NULL,tail = &head。 - 入队时,通过
*tail = new_node将新节点链接到队尾,然后更新tail = &(new_node->next)。 - 这种方式无需区分队列是否为空,代码简洁且高效。
2.3 线程池主体
struct thread_pool {
task_queue_t* queue; // 任务队列
pthread_t* threads; // 线程ID数组
int thread_count; // 线程数量
atomic_int quit; // 退出标志(原子操作)
};
3. 队列操作实现
3.1 创建队列(回滚式编程)
task_queue_t* task_queue_create() {
task_queue_t* q = malloc(sizeof(task_queue_t));
if (!q) return NULL;
q->head = NULL;
q->tail = &q->head; // 初始tail指向head的地址
q->block = 1; // 默认阻塞模式
if (pthread_spin_init(&q->spinlock, 0) != 0) {
free(q);
return NULL;
}
if (pthread_mutex_init(&q->mutex, NULL) != 0) {
pthread_spin_destroy(&q->spinlock);
free(q);
return NULL;
}
if (pthread_cond_init(&q->cond, NULL) != 0) {
pthread_mutex_destroy(&q->mutex);
pthread_spin_destroy(&q->spinlock);
free(q);
return NULL;
}
return q;
}
回滚式编程:每一步初始化失败时,必须释放之前已成功分配的资源,避免内存泄漏。
3.2 入队操作
void task_queue_push(task_queue_t* q, task_t* task) {
task->next = NULL; // 新节点的next置空
pthread_spin_lock(&q->spinlock);
*q->tail = task; // 链接到队尾
q->tail = &task->next; // 更新tail指向新节点的next
pthread_spin_unlock(&q->spinlock);
// 唤醒可能阻塞的消费者线程
pthread_mutex_lock(&q->mutex);
pthread_cond_signal(&q->cond);
pthread_mutex_unlock(&q->mutex);
}
3.3 出队操作(非阻塞)
task_t* task_queue_pop(task_queue_t* q) {
pthread_spin_lock(&q->spinlock);
task_t* task = q->head;
if (task) {
q->head = task->next;
if (q->head == NULL) // 队列变空,重置tail
q->tail = &q->head;
}
pthread_spin_unlock(&q->spinlock);
return task;
}
3.4 阻塞式获取任务(用于消费者线程)
task_t* task_queue_get(task_queue_t* q) {
task_t* task;
while (1) {
task = task_queue_pop(q);
if (task) return task;
// 队列为空,根据模式决定是否阻塞
if (!q->block) return NULL; // 非阻塞模式立即返回
pthread_mutex_lock(&q->mutex);
// 使用while循环防止虚假唤醒
while (q->head == NULL && q->block) {
pthread_cond_wait(&q->cond, &q->mutex);
}
pthread_mutex_unlock(&q->mutex);
}
}
注意:条件变量等待必须放在 while 循环中,因为可能存在虚假唤醒(即使没有信号,也可能返回)。
4. 线程池工作函数
static void* worker_routine(void* arg) {
thread_pool_t* pool = (thread_pool_t*)arg;
while (1) {
// 检查退出标志
if (atomic_load(&pool->quit)) break;
task_t* task = task_queue_get(pool->queue);
if (task) {
task->func(task->arg); // 执行任务
free(task); // 释放任务内存
}
}
return NULL;
}
5. 线程池创建与销毁
5.1 创建线程池
thread_pool_t* thread_pool_create(int thread_count) {
thread_pool_t* pool = malloc(sizeof(thread_pool_t));
if (!pool) return NULL;
pool->queue = task_queue_create();
if (!pool->queue) {
free(pool);
return NULL;
}
atomic_init(&pool->quit, 0);
pool->thread_count = thread_count;
pool->threads = malloc(sizeof(pthread_t) * thread_count);
if (!pool->threads) {
task_queue_destroy(pool->queue); // 需要实现销毁函数
free(pool);
return NULL;
}
for (int i = 0; i < thread_count; ++i) {
if (pthread_create(&pool->threads[i], NULL, worker_routine, pool) != 0) {
// 创建失败,销毁已创建的线程和资源
atomic_store(&pool->quit, 1); // 让已创建线程退出
for (int j = 0; j < i; ++j) {
pthread_join(pool->threads[j], NULL);
}
free(pool->threads);
task_queue_destroy(pool->queue);
free(pool);
return NULL;
}
}
return pool;
}
5.2 提交任务
int thread_pool_post(thread_pool_t* pool, void (*func)(void*), void* arg) {
if (atomic_load(&pool->quit)) return -1; // 线程池已关闭
task_t* task = malloc(sizeof(task_t));
if (!task) return -1;
task->func = func;
task->arg = arg;
task_queue_push(pool->queue, task);
return 0;
}
5.3 销毁线程池
void thread_pool_destroy(thread_pool_t* pool) {
if (!pool) return;
atomic_store(&pool->quit, 1); // 设置退出标志
// 唤醒所有等待的线程
pthread_mutex_lock(&pool->queue->mutex);
pthread_cond_broadcast(&pool->queue->cond);
pthread_mutex_unlock(&pool->queue->mutex);
// 等待所有线程结束
for (int i = 0; i < pool->thread_count; ++i) {
pthread_join(pool->threads[i], NULL);
}
// 释放资源
task_queue_destroy(pool->queue);
free(pool->threads);
free(pool);
}
6. 队列销毁函数
void task_queue_destroy(task_queue_t* q) {
// 清空剩余任务
task_t* task = task_queue_pop(q);
while (task) {
free(task);
task = task_queue_pop(q);
}
pthread_spin_destroy(&q->spinlock);
pthread_mutex_destroy(&q->mutex);
pthread_cond_destroy(&q->cond);
free(q);
}
五、线程池的测试与优化
1. 单元测试(使用Google Test)
由于我们的队列是线程池的核心,可以单独测试其逻辑:
#include <gtest/gtest.h>
extern "C" {
#include "task_queue.h"
}
TEST(TaskQueueTest, PushPop) {
task_queue_t* q = task_queue_create();
task_t t1, t2;
t1.next = NULL;
t2.next = NULL;
task_queue_push(q, &t1);
task_queue_push(q, &t2);
EXPECT_EQ(task_queue_pop(q), &t1);
EXPECT_EQ(task_queue_pop(q), &t2);
EXPECT_EQ(task_queue_pop(q), nullptr);
task_queue_destroy(q);
}
编译时需链接pthread和gtest。
2. 线程池功能测试
编写一个简单程序测试线程池:
#include "thread_pool.h"
#include <stdio.h>
#include <unistd.h>
void task_func(void* arg) {
int* num = (int*)arg;
printf("Task %d is running\n", *num);
sleep(1); // 模拟耗时操作
printf("Task %d finished\n", *num);
free(arg);
}
int main() {
thread_pool_t* pool = thread_pool_create(4);
for (int i = 0; i < 10; ++i) {
int* arg = malloc(sizeof(int));
*arg = i;
thread_pool_post(pool, task_func, arg);
}
sleep(3); // 等待任务完成
thread_pool_destroy(pool);
return 0;
}
编译时需要指定动态库路径:
gcc -o test test.c -L. -lthreadpool -lpthread -Wl,-rpath='$ORIGIN'
3. 性能优化考虑
- 锁的选择:自旋锁适用于短临界区,但如果线程被抢占且持有锁,其他等待线程会忙等,浪费CPU。可以改用互斥锁,或者结合两者(如先自旋几次再睡眠)。
- 任务队列大小:如果任务产生速度远大于消费速度,队列会无限增长,导致内存压力。可设置最大队列长度,当队列满时,生产者可选择阻塞或返回错误。
- 工作线程数量:通过压测找到最佳线程数,考虑任务类型、CPU核心数、IO等待时间等因素。

浙公网安备 33010602011771号