1. 线程池原理
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。
线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
线程池的作用主要是避免因频繁创建和销毁线程带来的系统效率降低问题,实现线程复用。当并发线程数量多且线程执行任务时间短的情况下,频繁创建和销毁线程会消耗时间,而线程池可以让线程在执行完一个任务后不被销毁,继续执行其他任务,以此提高系统效率 。
Important
线程池的实现原理:
线程池的组成主要分为3个部分,配合工作可以得到一个完整的线程池:
1.任务队列,存储需要处理的任务,由工作的线程来处理这些任务
- 通过线程池提供的API函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除
- 已处理的任务会被从任务队列中删除
- 线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程**
2.工作的线程(任务队列任务的消费者) ,N个
- 线程池中维护了一定数量的工作线程, 他们的作用是是不停的读任务队列, 从里边取出任务并处理
- 工作的线程相当于是任务队列的消费者角色,
- 如果任务队列为空, 工作的线程将会被阻塞 (使用条件变量/信号量阻塞)
- 如果阻塞之后有了新的任务, 由生产者将阻塞解除, 工作线程开始工作
3.管理者线程(不处理任务队列中的任务,1个
- 它的任务是周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测
- 当任务过多的时候, 可以适当的创建一些新的工作线程
- 当任务过少的时候, 可以适当的销毁一些工作的线程
2. 任务队列
// 任务结构体
typedef struct Task
{
void (*function)(void* arg);
//函数指针是一种特殊的指针,它**指向一个函数的地址**,可以通过它来调用函数。
//换句话说,**函数指针就是一个存储函数地址的变量**,它允许我们像操作普通指针一样操作函数,并可以在运行时动态调用不同的函数。
void* arg;
}Task;
线程池的 taskQ(任务队列)是一个数组,用于存储待执行的任务。例如:
pool->taskQ[0] = { functionA, argA };
pool->taskQ[1] = { functionB, argB };
pool->taskQ[2] = { functionC, argC };
pool->taskQ[i] 表示第 i 个任务
Note
每个任务包含:
- function:函数指针,指向具体任务(例如 functionA)
- arg:任务执行时所需的参数(例如 argA)
queueFront
指向队列的头部
3. 线程池定义
// 线程池结构体
struct ThreadPool
{
// 任务队列
Task* taskQ;
int queueCapacity; // 容量
int queueSize; // 当前任务个数
int queueFront; // 队头 -> 取数据
int queueRear; // 队尾 -> 放数据
pthread_t managerID; // 管理者线程ID
pthread_t *threadIDs; // 工作的线程ID
int minNum; // 最小线程数量
int maxNum; // 最大线程数量
int busyNum; // 忙的线程的个数
int liveNum; // 存活的线程的个数
int exitNum; // 要销毁的线程个数
pthread_mutex_t mutexPool; // 锁整个的线程池
pthread_mutex_t mutexBusy; // 锁busyNum变量
pthread_cond_t notFull; // 任务队列是不是满了
pthread_cond_t notEmpty; // 任务队列是不是空了
int shutdown; // 是不是要销毁线程池, 销毁为1, 不销毁为0
};
4. 头文件声明
#ifndef _THREADPOOL_H
#define _THREADPOOL_H
typedef struct ThreadPool ThreadPool;
// 创建线程池并初始化
ThreadPool *threadPoolCreate(int min, int max, int queueSize);
// 销毁线程池
int threadPoolDestroy(ThreadPool* pool);
// 给线程池添加任务
void threadPoolAdd(ThreadPool* pool, void(*func)(void*), void* arg);
// 获取线程池中工作的线程的个数
int threadPoolBusyNum(ThreadPool* pool);
// 获取线程池中活着的线程的个数
int threadPoolAliveNum(ThreadPool* pool);
//
// 工作的线程(消费者线程)任务函数
void* worker(void* arg);
// 管理者线程任务函数
void* manager(void* arg);
// 单个线程退出
void threadExit(ThreadPool* pool);
#endif // _THREADPOOL_H
5. 源文件定义
const int NUMBER = 2;
ThreadPool* threadPoolCreate(int min, int max, int queueSize)
{
ThreadPool* pool = (ThreadPool*)malloc(sizeof(ThreadPool));
do
{
if (pool == NULL)
{
printf("malloc threadpool fail...\n");
break;//跳出
}
//分配内存地址给pool结构体中的threadIDs成员变量
pool->threadIDs = (pthread_t*)malloc(sizeof(pthread_t) * max);
if (pool->threadIDs == NULL)
{
printf("malloc threadIDs fail...\n");
break;
}
//`void *memset(void *s, int c, size_t n);`
// `s`:指向要填充的内存区域的指针。
// `c`:要填充的值(会被转换为 `unsigned char` 类型)。
// `n`:要填充的字节数。
memset(pool->threadIDs, 0, sizeof(pthread_t) * max);
pool->minNum = min;
pool->maxNum = max;
pool->busyNum = 0;
pool->liveNum = min; // 和最小个数相等
pool->exitNum = 0;
//检查多个初始化操作是否成功
if (pthread_mutex_init(&pool->mutexPool, NULL) != 0 ||
pthread_mutex_init(&pool->mutexBusy, NULL) != 0 ||
pthread_cond_init(&pool->notEmpty, NULL) != 0 ||
pthread_cond_init(&pool->notFull, NULL) != 0)
{
printf("mutex or condition init fail...\n");
break;
}
// 任务队列初始化
pool->taskQ = (Task*)malloc(sizeof(Task) * queueSize);
pool->queueCapacity = queueSize;
pool->queueSize = 0;
pool->queueFront = 0;
pool->queueRear = 0;
pool->shutdown = 0;
// 创建线程
pthread_create(&pool->managerID, NULL, manager, pool);
for (int i = 0; i < min; ++i)
{
pthread_create(&pool->threadIDs[i], NULL, worker, pool);
}
return pool;
} while (0);
// 释放资源
// 如果线程池结构体存在,并且线程ID数组存在,释放线程ID数组内存
if (pool && pool->threadIDs)
free(pool->threadIDs);
// 如果线程池结构体存在,并且任务队列存在,释放任务队列内存
if (pool && pool->taskQ)
free(pool->taskQ);
// 最后释放线程池结构体本身的内存
if (pool)
free(pool);
return NULL;
}
int threadPoolDestroy(ThreadPool* pool)
{
if (pool == NULL)
{
return -1;
}
// 关闭线程池
pool->shutdown = 1;
// 阻塞、等待回收管理者线程
pthread_join(pool->managerID, NULL);
// 唤醒阻塞的消费者线程
for (int i = 0; i < pool->liveNum; ++i)
{
pthread_cond_signal(&pool->notEmpty);
}
// 释放堆内存
if (pool->taskQ)//不为空
{
free(pool->taskQ);
}
if (pool->threadIDs)//不为空
{
free(pool->threadIDs);
}
pthread_mutex_destroy(&pool->mutexPool);
pthread_mutex_destroy(&pool->mutexBusy);
pthread_cond_destroy(&pool->notEmpty);
pthread_cond_destroy(&pool->notFull);
free(pool);//释放pool
pool = NULL;
return 0;
}
//生产者添加--将一个新的任务添加到线程池的任务队列
void threadPoolAdd(ThreadPool* pool, void(*func)(void*), void* arg)
{
pthread_mutex_lock(&pool->mutexPool);
//如果任务队列已满,并且线程池没有关闭,就阻塞当前的生产者线程
while (pool->queueSize == pool->queueCapacity && !pool->shutdown)
{
// 当队列等于队列容量 阻塞生产者线程 消费者唤醒生产者线程
pthread_cond_wait(&pool->notFull, &pool->mutexPool);
}
//线程池是否已经关闭了
if (pool->shutdown)
{
pthread_mutex_unlock(&pool->mutexPool);
return;
}
// 添加任务 在队列队尾添加
pool->taskQ[pool->queueRear].function = func;
pool->taskQ[pool->queueRear].arg = arg;
//形成环形队列
pool->queueRear = (pool->queueRear + 1) % pool->queueCapacity;
pool->queueSize++;
pthread_cond_signal(&pool->notEmpty);
pthread_mutex_unlock(&pool->mutexPool);
}
int threadPoolBusyNum(ThreadPool* pool)
{
pthread_mutex_lock(&pool->mutexBusy);
int busyNum = pool->busyNum;
pthread_mutex_unlock(&pool->mutexBusy);
return busyNum;
}
int threadPoolAliveNum(ThreadPool* pool)
{
pthread_mutex_lock(&pool->mutexPool);
int aliveNum = pool->liveNum;
pthread_mutex_unlock(&pool->mutexPool);
return aliveNum;
}
void* worker(void* arg)
{
ThreadPool* pool = (ThreadPool*)arg; // 获取线程池指针
while (1) // 工作线程循环执行任务
{
pthread_mutex_lock(&pool->mutexPool); // 先加锁,访问任务队列是临界区
// ===================== 等待任务的到来 =====================
// 当前任务队列为空并且线程池没有关闭,就阻塞等待
while (pool->queueSize == 0 && !pool->shutdown)
{
// 阻塞工作线程,直到有任务被添加(生产者发出 notEmpty 信号)
//等待自动上锁,唤醒自动加锁。
pthread_cond_wait(&pool->notEmpty, &pool->mutexPool);
// ========== 判断是否需要自杀(缩容线程池)==========
if (pool->exitNum > 0)
{
pool->exitNum--; // 自杀名额减1
if (pool->liveNum > pool->minNum)
{
pool->liveNum--; // 活着的线程数减1
pthread_mutex_unlock(&pool->mutexPool); // 先解锁
threadExit(pool); // 自杀:让线程退出
}
}
}
// ===================== 判断是否要关闭线程池 =====================
if (pool->shutdown)
{
pthread_mutex_unlock(&pool->mutexPool);
threadExit(pool); // 执行清理后退出线程
}
// ===================== 正式取任务 =====================
Task task;
// 拿到任务队列中头部的任务(先进先出)
task.function = pool->taskQ[pool->queueFront].function;
task.arg = pool->taskQ[pool->queueFront].arg;
// 移动头节点,实现循环队列的头部向后移动
pool->queueFront = (pool->queueFront + 1) % pool->queueCapacity;
pool->queueSize--; // 当前任务数量减1
// 有空位了,唤醒生产者线程
pthread_cond_signal(&pool->notFull);
// 解锁任务队列
pthread_mutex_unlock(&pool->mutexPool);
// ===================== 执行任务前 =====================
printf("thread %ld start working...\n", pthread_self());
// 标记为忙碌线程(加锁保护 busyNum)
pthread_mutex_lock(&pool->mutexBusy);
pool->busyNum++;
pthread_mutex_unlock(&pool->mutexBusy);
// ===================== 执行任务 =====================
task.function(task.arg); // 执行任务函数
free(task.arg); // 释放任务参数的内存
task.arg = NULL;
// ===================== 执行任务后 =====================
printf("thread %ld end working...\n", pthread_self());
// 减少忙碌线程数
pthread_mutex_lock(&pool->mutexBusy);
pool->busyNum--;
pthread_mutex_unlock(&pool->mutexBusy);
}
return NULL;
}
void* manager(void* arg)
{
ThreadPool* pool = (ThreadPool*)arg;
while (!pool->shutdown)
{
// 每隔3s检测一次
sleep(3);
// 取出线程池中任务的数量和当前线程的数量
pthread_mutex_lock(&pool->mutexPool);
int queueSize = pool->queueSize;
int liveNum = pool->liveNum;
pthread_mutex_unlock(&pool->mutexPool);
// 取出忙的线程的数量
pthread_mutex_lock(&pool->mutexBusy);
int busyNum = pool->busyNum;
pthread_mutex_unlock(&pool->mutexBusy);
// 添加线程
// 任务的个数>存活的线程个数 && 存活的线程数<最大线程数
if (queueSize > liveNum && liveNum < pool->maxNum)
{
pthread_mutex_lock(&pool->mutexPool);
int counter = 0;
for (int i = 0; i < pool->maxNum && counter < NUMBER
&& pool->liveNum < pool->maxNum; ++i)
{
if (pool->threadIDs[i] == 0)
//当线程退出时,线程池可能会将该线程的位置标记为 `0`,表示该线程已经不再有效
{
pthread_create(&pool->threadIDs[i], NULL, worker, pool);
counter++;
pool->liveNum++;
}
}
pthread_mutex_unlock(&pool->mutexPool);
}
// 销毁线程
// 忙的线程*2 < 存活的线程数 && 存活的线程>最小线程数
if (busyNum * 2 < liveNum && liveNum > pool->minNum)
{
//mutexPool临界区是包括什么?
pthread_mutex_lock(&pool->mutexPool);
pool->exitNum = NUMBER;
pthread_mutex_unlock(&pool->mutexPool);
// 让工作的线程自杀
for (int i = 0; i < NUMBER; ++i)
{
pthread_cond_signal(&pool->notEmpty);
}
}
}
return NULL;//终止函数并返回一个空指针,销毁管理者线程
}
void threadExit(ThreadPool* pool)
{
pthread_t tid = pthread_self();
for (int i = 0; i < pool->maxNum; ++i)
{
if (pool->threadIDs[i] == tid)
{
pool->threadIDs[i] = 0;
printf("threadExit() called, %ld exiting...\n", tid);
break;
}
}
pthread_exit(NULL);
}
Note
- 遍历线程池中的线程 ID 数组
pool->threadIDs
,查找当前线程的 ID。 - 如果找到了当前线程的 ID(即
pool->threadIDs[i] == tid
),则将该位置的线程 ID 设置为 0,表示该线程已经退出。
6. 测试代码
void taskFunc(void* arg)
{
int num = *(int*)arg;
printf("thread %ld is working, number = %d\n",pthread_self(), num);
sleep(1);
}
int main()
{
// 创建线程池
ThreadPool* pool = threadPoolCreate(3, 10, 100);
for (int i = 0; i < 100; ++i)
{
int* num = (int*)malloc(sizeof(int));
*num = i + 100;//指定任务队列区域,从100-199
threadPoolAdd(pool, taskFunc, num);
}
sleep(30);
threadPoolDestroy(pool);
return 0;
}
在关闭线程池时,正确的操作顺序是先设置 shutdown
标志,再调用 pthread_join
等待管理者线程退出。
// 关闭线程池
pool->shutdown = 1;
// 阻塞回收管理者线程
pthread_join(pool->managerID, NULL);
原因:
- 先设置
shutdown
,再pthread_join
:- 设置
shutdown
为 1 之后,线程池内的线程(包括管理者线程)会检查到该标志,知道应该退出。这保证了线程池在关闭时的稳定性。 pthread_join
会阻塞当前线程,直到管理者线程退出,确保所有线程安全终止。
- 设置
- 先
pthread_join
,再设置shutdown
:- 如果先
pthread_join
,管理者线程可能在未收到关闭信号的情况下继续执行,导致关闭过程混乱,甚至可能出现死锁或线程无法正确终止的情况。
- 如果先