554
社区成员
发帖
与我相关
我的任务
分享
在学习多线程编程时,会有一些问题:
1 单线程无法高效处理多个任务。
2 频繁创建线程开销较大。
因此,引入“线程池”来解决问题:
线程复用 提高性能 控制并发数量
本文将从零实现一个基于 pthread 的线程池,并详细解析源码。
为什么要有线程池?
如果没有线程池,处理问题的大概流程是这样的:
1.pthread_create 创建一个线程
2.线程执行任务
3线程退出
4.再来一个任务,再创建一个线程
5.再执行 再退出
会出现频繁创建线程花销很大的问题,而且任务数量变多,会很乱
因此引入线程池。
结构体1:任务结构体nTask,它表示一个待执行的任务
struct nTask{ //一个任务
void(*task_func)(struct nTask *task); //函数指针 表示真正要执行的函数是什么
void *user_data; //执行的函数传入的参数
struct nTask *prev;
struct nTask *next; //一起构成任务队列
};
/*task_func:任务执行函数(函数指针),表示这个任务真正要执行的任务是谁
user_data : 表示这个任务执行时要带什么参数。表示任务不是空跑,它是带着数据去执行的
prev和next两个指针:将一个任务和其他的任务串联起来,构成一个任务队列*/
结构体2:Worker结构体,表示线程池里的一个工作线程节点
struct nWorker { //一个线程
pthread_t threadid; //线程id
int terminate; //终止标记
struct nManager *manager;//该线程所属的线程池管理器
struct nWorker *prev;
struct nWorker *next; //一起构成一个worker链表
};
/*threadid:线程ID
terminate:终止标记,标记这个线程是否退出
manager:指向所属的线程池 让每个worker都能回到自己所属的线程池去拿任务。
线程池也有自己的线程池链表*/
结构体3:线程池管理器 nManager / ThreadPool
typedef struct nManager{ //线程池管理器
struct nTask *tasks; //任务队列
struct nWorker *workers;//线程队列
pthread_mutex_t mutex; //互斥锁
pthread_cond_t cond; //条件变量,有任务时,唤醒线程
}ThreadPool;
/*线程池管理器管理任务struct nTask *tasks这个指向任务链表 当前有哪些任务还没做都挂在这条链表上
也管理工作线程 线程池里有哪些线程 都挂在这条链表上
因为多个线程会同时访问任务队列:主线程会往里面塞任务 worker线程会从里面拿任务 所以必须加锁。
pthread_cond_t cond的作用是:当任务队列为空时,让 worker 睡眠等待;一旦有新任务,再把 worker 唤醒。
如果没有这个cond条件变量,worker线程会一直在while循环里询问有没有任务,非常浪费资源
所以更合理的做法是:没任务:睡觉 有任务:叫醒*/
为了高效管理任务队列和线程队列,使用双向链表。
LIST_INSERT:头插法
LIST_REMOVE:删除节点
#define LIST_INSERT(item,list)do{ \
item->prev = NULL; \
item->next = list; \
if((list) != NULL) (list)->prev = item; \
(list) = item; \
}while(0) //头插法 插入节点到链表最前面
#define LIST_REMOVE(item,list)do{ \
if(item->prev != NULL) item->prev->next = item->next; \
if(item->next != NULL) item->next->prev = item->prev; \
if(list == item) list = item->next; \
item->prev = item->next = NULL; \
}while(0)//节点item从list中删除
插入执行过程:
1. 新节点前驱置空
2. 新节点后继指向原表头
3. 如果原表头存在,则修改原表头的前驱
4. 更新链表头指针
删除执行过程:
1. 修复前驱节点的 next
2. 修复后继节点的 prev
3. 如果删除的是表头,则更新表头指针
nThreadPoolCallback:每一个 worker 线程启动后反复执行的主循环函数
static void *nThreadPoolCallback(void *arg){ //工作线程
struct nWorker *worker = (struct nWorker*)arg; //取出线程自己 创建线程的时候传进来的参数就是 worker 自己,所以这里先转回来。
while(1){ //线程池线程:反复取任务,长期复用 所以是一个循环
pthread_mutex_lock(&worker->manager->mutex); //先加锁 因为任务队列 tasks 是共享资源,多个线程都可能同时访问,所以必须先加锁。
while(worker->manager->tasks == NULL){ //没有任务情况的思路:进入条件变量等待队列,睡眠 等有人塞任务进来,再被唤醒
if(worker->terminate) break; //被通知退出 break
pthread_cond_wait(&worker->manager->cond,&worker->manager->mutex); //当前线程放入等待队列 锁释放
}
if(worker->terminate){
pthread_mutex_unlock(&worker->manager->mutex); //被通知退出了 解锁加break
break;
}
struct nTask *task = worker->manager->tasks; //取出任务
LIST_REMOVE(task,worker->manager->tasks); //任务从链表中移除
pthread_mutex_unlock(&worker->manager->mutex); //先解锁 再执行任务 锁只保护取任务这个动作 在锁外执行任务
task->task_func(task); //真正执行想要执行的任务 调用任务对应的函数。
}
free(worker); //循环结束 释放worker
return NULL;
}
/*这是每个工作线程真正执行的函数 nThreadPoolCallback是每个线程一直在跑的主循环。
核心执行逻辑:线程启动->加锁->检查任务队列是否为空->如果为空就睡眠等待->如果有任务就取出来
->解锁->执行任务->再次循环
这里“取任务在锁内、执行任务在锁外”是一个关键设计。因为任务执行时间可能较长,如果一直持有锁,会严重降低线程池的并发能力。*/
nThreadPoolCreate:创建并初始化一个线程池
int nThreadPoolCreate(ThreadPool *pool,int numWorkers){
if(pool == NULL) return -1;
if(numWorkers < 1) numWorkers = 1; //线程数至少是1
memset(pool,0,sizeof(ThreadPool)); //清空线程池
pthread_cond_t blank_cond = PTHREAD_COND_INITIALIZER;
memcpy(&pool->cond,&blank_cond,sizeof(pthread_cond_t));
//等价于pthread_cond_init(&pool->cond, NULL); 初始化条件变量
pthread_mutex_t blank_mutex = PTHREAD_MUTEX_INITIALIZER;
memcpy(&pool->mutex, &blank_mutex, sizeof(pthread_mutex_t));
//等价于pthread_mutex_init(&pool->mutex,NULL); 初始化锁
int i = 0;
for(i = 0;i < numWorkers;i++){ //创建worker 每个线程都配一个 worker 结构体,用来存它自己的状态。
struct nWorker *worker = (struct nWorker*)malloc(sizeof(struct nWorker)); // 申请worker内存
if(worker == NULL){
perror("malloc");
return -2;
}
memset(worker,0,sizeof(struct nWorker));
worker->manager = pool; //记录worker属于哪个线程池 线程启动后通过worker->manager找回自己的线程池。
int ret = pthread_create(&worker->threadid,NULL,nThreadPoolCallback,worker); //真正启动一个线程,让它执行nThreadPoolCallback。
if(ret){
perror("pthread_create");
free(worker);
return -3;
}
LIST_INSERT(worker,pool->workers); //把worker挂到链表里 这样线程池就能统一管理所有工作线程了。
}
return 0; //成功
}
/*创建并初始化一个线程池 并创建若干线程 在线程池使用之前调用。
也就是:在线程池正式工作之前 需要初始化的部分全都初始化任务队列初始化:
线程队列初始化 互斥锁初始化 条件变量初始化 创建若干工作线程
创建完成后,这些线程会立即启动,并进入等待任务的状态*/
nThreadPoolPushTask:销毁线程池
int nThreadPoolDestroy(ThreadPool *pool){ //销毁线程池 通知所有线程退出
struct nWorker *worker = NULL;
for(worker = pool->workers;worker != NULL;worker = worker->next){
worker->terminate = 1; //通知所有线程退出
}
pthread_mutex_lock(&pool->mutex); //锁 然后广播唤醒所有的线程 再解锁
pthread_cond_broadcast(&pool->cond);
pthread_mutex_unlock(&pool->mutex);
pool->workers = NULL;
pool->tasks = NULL;
return 0;
}
//该函数进行了线程池的销毁 通知所有线程退出
//目前只进行了设置退出标志、唤醒线程的简单演示,实际应该销毁mutex 销毁cond 清理未执行完的任务节点
nThreadPoolPushTask:把一个任务提交到线程池
int nThreadPoolPushTask(ThreadPool *pool,struct nTask *task){ //把一个任务提交到线程池中
pthread_mutex_lock(&pool->mutex); //加锁 因为tasks是共享资源
LIST_INSERT(task,pool->tasks); //在任务列表插入task
pthread_cond_signal(&pool->cond); //通知线程来做任务
pthread_mutex_unlock(&pool->mutex); //开锁
return 0; // 补充返回值
}
/*将一个任务提交给线程池 主线程创建好任务后,会通过该函数将任务压入线程池。
这样线程池中的工作线程就可以及时获取到新任务。*/
task_entry:真正的需要执行的函数
线程池只负责“调度任务”,而任务的具体执行逻辑由 task_func 指向的函数决定。
#define THREADPOOL_INIT_COUNT 20
#define TASK_INIT_SIZE 1000
void task_entry(struct nTask *task){ //测试函数
//struct nTask *task = (struct nTask*)task;
int idx = *(int *)task->user_data; //user_data 里面放了一个int*,先转回int*,再解引用 拿到值
printf("idx: %d\n",idx);
free(task->user_data);
free(task);
//释放内存
}
/*一个具体任务的执行函数,这里只做演示*/
main函数:
int main(void){
ThreadPool pool = {0}; //定义一个线程池管理对象 初始化0
nThreadPoolCreate(&pool,THREADPOOL_INIT_COUNT); //创建 20 个工作线程 这些线程一启动就会进入等待状态,等待任务
int i = 0;
for(i = 0;i < TASK_INIT_SIZE;i++){ //循环创建任务
struct nTask *task = (struct nTask *)malloc(sizeof(struct nTask)); //申请任务对象
if(task == NULL){
perror("malloc");
exit(1);
}
memset(task,0,sizeof(struct nTask)); //清零
task->task_func = task_entry; //指定任务函数
task->user_data = malloc(sizeof(int)); //分配内存
*(int *)task->user_data = i; // 指定任务参数
nThreadPoolPushTask(&pool,task); //任务放入任务队列
}
getchar(); // 按回车退出程序
nThreadPoolDestory(&pool); //销毁线程池
return 0;
}
/*整个线程池程序的运行过程如下:
1. main 函数创建线程池
2. 线程池内部创建多个 worker 线程
3. worker 线程启动后进入等待状态
4. main 函数不断创建任务并压入任务队列
5. 有任务到来时,通过条件变量唤醒工作线程
6. 工作线程取出任务并执行对应的 task_func
7. 执行完成后继续等待新的任务
8. 销毁线程池时,设置 terminate 标记并广播唤醒所有线程退出*/
从整体来看,这个线程池主要完成了以下几个核心功能:
1使用任务队列管理待执行任务
2使用线程池实现线程复用
3通过mutex和cond实现线程间同步与通信
4使用条件变量避免线程空转,提高 CPU 利用率
-----从0开始手写了一个小的可实现的线程池
学习资源:https://github.com/0voice
我的第一篇博客哈哈哈,从这里开始记录我的大学生活~