基于c语言实现一个线程池以及线程池基本原理

📅 2026/7/11 5:02:20 👁️ 阅读次数 📝 编程学习
基于c语言实现一个线程池以及线程池基本原理

在POSIX API的标准下,一个线程需要8M的内存空间,1024M的内存空间可以开辟128个线程。
如果是16G的内存可以开16×128 = 2048个线程。显然一次性开辟大量的线程对我们的计算机资源的消耗是十分大的。
为了避免这种消耗,我们可以使用线程池,线程池可以提前开辟一些线程,后续如果又任务产生,则可以通过调度线程池中的空闲的线程来直接执行对应的任务。

1 线程池组成

1.1 任务队列

任务队列的包括了任务数据,以及对应回调函数,该回调函数用于对任务数据进行消费,根据需求来处理该任务数据,单个任务的结构体定义如下:

typedefstructTask{void(*task_handler)(void*data);//回调函数,用于处理任务数据void*task_data;//任务数据structTask*next;//指向下一个任务}Task;

1.2 工作队列

工作队列中的每个节点就是我们创建的线程,每个线程都需要从任务队列中提取任务,然后通过每个任务的回调函数来处理这个任务。
每个工作队列的的结构体定义如下:

typedefstructWorker{pthread_tthread_id;//工作的线程idstructManager*manager;//指向管理者structWorker*next;//下一个工作线程structWorker*prev;//上一个工作线程}Worker;

1.3 管理者

管理者是线程池的调度中枢,它不直接处理任务,而是通过锁 + 条件变量实现对任务队列和工作线程的线程安全协调。其结构体定义如下:

typedefstructManager{structTaskQueue*task_queue;// 任务队列(FIFO)structWorkerQueue*worker_queue;// 工作线程队列pthread_mutex_tmutex;// 互斥锁,保护队列和状态pthread_cond_tcond;// 条件变量,用于阻塞/唤醒工作线程intshutdown;// 销毁标志,1 表示线程池即将关闭}Manager;

管理者是调度任务队列和工作队列的组件,是让线程池可以正常运转的核心,通过管理者,我们可以知道哪些工作线程是空闲的,任务来了就通知工作线程来处理。还有哪些任务正在被工作线程处理,正在处理的任务就不用通知别的工作线程处理了。

在整个调度过程中,任务队列和工作队列不直接沟通,需要通过管理者来协调。比如当你去高档饭店吃饭,后厨有厨师长和许多的厨师,厨师长可以理解为这里的管理者,厨师就是工作线程,当菜单栏(任务队列)没有任务的时候所有的厨师可以坐着休息,当来了菜单,厨师长(管理者)就协调一个人来做菜。

结合上述定义的结构体,锁mutex用于确保做菜的厨师们(工作线程)不会乱,比如一个厨师(工作线程)做了西红柿炒蛋这道菜,并且菜单栏(任务队列)上没有这道菜了,其他厨师(工作线程)就不会做了。

对于条件变量cond,可以类似为一个指示灯,如果厨师长(管理者)发现新的菜单入队,就唤醒条件变量cond(打开指示灯),通知一个厨师(工作线程)来拿到这个任务,没有任务则关闭指示灯,所有的厨师可以直接休息。除此之外,厨师长知道饭店打烊了,就通过指示灯来通知所有的厨师不用准备了,直接下班。

示意图如下:

  1. 当菜单没有菜名的时候,所有的厨师可以坐着休息下,不用在灶台前等候
  2. 但菜单栏有菜名的时候,厨师长通过条件变量来告诉厨师取任务
  3. 厨师取到自己需要做的菜名,但是每个菜都有自己的做法,厨师需要根据菜名来做,对应我们的任务队列中的每个任务都有处理任务数据的回调函数,通过这个回调函数来处理任务数据

2 接口实现

2.1 线程池的初始化

线程池的初始化参数包含了管理者以及需要开辟的工作线程个数,主要是对管理者的相关参数进行设置以及通过pthread_create()来创建每个工作队列节点的线程。

/** * @brief 创建并初始化线程池 * @param manager 管理者结构体指针(调用者分配内存) * @param worker_count 要创建的工作线程数量 * @return 0 成功,-1 参数错误,-2 工作线程结构体分配失败,-3 线程创建失败 */intthreadpool_create(Manager*manager,size_tworker_count){// 1. 参数校验:管理者指针不能为空,线程数量至少为1if(!manager||worker_count<1)return-1;// 2. 初始化基本状态:关闭标志清0,表示线程池正在运行manager->shutdown=0;// 3. 为任务队列和工作队列分配内存manager->task_queue=malloc(sizeof(TaskQueue));// 任务队列结构体manager->worker_queue=malloc(sizeof(WorkerQueue));// 工作线程队列结构体// 4. 队列初始化为空(头尾指针置空)manager->task_queue->head=NULL;manager->task_queue->tail=NULL;manager->worker_queue->head=NULL;// 5. 初始化互斥锁和条件变量,用于线程安全调度pthread_cond_init(&manager->cond,NULL);// 条件变量,默认属性pthread_mutex_init(&manager->mutex,NULL);// 互斥锁,默认属性// 6. 循环创建指定数量的工作线程for(size_ti=0;i<worker_count;i++){// 6.1 为单个工作线程结构体动态分配内存Worker*sin_worker=malloc(sizeof(Worker));if(!sin_worker){perror("malloc");// 打印分配失败原因free(sin_worker);// 释放可能部分分配的内存(此处为空)return-2;// 返回分配失败错误码}// 6.2 让工作线程知道自己属于哪个管理者sin_worker->manager=manager;// 6.3 创建线程,入口函数为 worker_threadif(pthread_create(&sin_worker->thread_id,NULL,worker_thread,sin_worker)){perror("pthread create");// 打印线程创建失败原因free(sin_worker);// 释放已分配的工作线程节点return-3;// 返回线程创建失败错误码}// 6.4 将新创建的工作线程节点插入到工作队列头部(链表头插法)// LIST_INSERT 是一个自定义宏,功能为将第一个参数节点插入第二个参数链表头部LIST_INSERT(sin_worker,manager->worker_queue->head);}return0;}

2.2 工作线程的实现

每个工作队列节点都对应一个后台的线程用于从任务队列拿出任务数据,然后进行处理。

/** * @brief 工作线程入口函数(每个工作线程启动时执行) * @param arg 指向当前工作线程对应的 Worker 结构体 * @return NULL */staticvoid*worker_thread(void*arg){// 1. 获取当前线程的管理信息(包含任务队列、锁、条件变量等)Worker*sin_worker=(Worker*)arg;while(1)// 工作线程主循环,持续等待和处理任务{// 2. 加锁:所有对任务队列和关闭标志的操作必须在互斥锁保护下进行pthread_mutex_lock(&sin_worker->manager->mutex);// 3. 等待任务到来或线程池关闭信号// 使用 while 而非 if 是为了防止虚假唤醒(spurious wakeup)while(!sin_worker->manager->task_queue->head&&!sin_worker->manager->shutdown){// 3.1 条件变量阻塞:释放 mutex 并进入等待,直到被 signal 或 broadcast 唤醒pthread_cond_wait(&sin_worker->manager->cond,&sin_worker->manager->mutex);// 被唤醒后会重新获得锁,再次检查条件是否满足}// 4. 取出任务队列的第一个任务(FIFO)Task*sin_task=sin_worker->manager->task_queue->head;// 5. 处理关闭场景:如果任务队列为空且关闭标志被设置,表示线程池即将销毁if(!sin_task&&sin_worker->manager->shutdown){pthread_mutex_unlock(&sin_worker->manager->mutex);// 释放锁再退出break;// 跳出循环,线程正常结束}// 6. 从队列中移除该任务(更新头指针,若队列变空则尾指针也置空)sin_worker->manager->task_queue->head=sin_task->next;if(!sin_worker->manager->task_queue->head)sin_worker->manager->task_queue->tail=NULL;// 7. 解锁:对队列的修改已完成,释放锁让其他线程可以操作队列pthread_mutex_unlock(&sin_worker->manager->mutex);// 8. 执行任务:调用该任务预先设置的回调函数,传入任务数据sin_task->task_handler(sin_task->task_data);// 注意:任务结构体(sin_task)通常由主线程分配,这里执行完后应由主线程或其他机制释放,// 或者在回调函数内自行释放。此示例中未在这里释放,可根据具体设计在回调中释放。}returnNULL;}

2.3 任务队列中处理任务数据的回调函数

将任务队列中的任务数据的回调函数写好,这样当我工作线程拿到了任务,直接调用对应的回调函数就可以完成任务数据的处理。
这里以打印接收到的任务数据为例。

voidprint_task_data(void*data){intrecv_data=*(int*)data;printf("the salary is %d\n",recv_data);}

2.4 给任务队列总加入任务

加入任务就是将单节点插入到任务队列中。

/** * @brief 向线程池的任务队列中添加一个任务,并唤醒一个工作线程去执行它 * @param manager 线程池管理者指针 * @param sin_task 待添加的任务节点(由调用者分配内存并设置好回调函数和数据) * @return 0 成功,-1 参数错误,-2 线程池正在关闭中无法添加任务 */intthreadpool_addtask(Manager*manager,Task*sin_task){// 1. 参数校验:管理者、任务以及任务队列都必须存在if(!manager||!sin_task||!manager->task_queue)return-1;// 2. 加锁:后续对任务队列和关闭标志的修改需要互斥保护pthread_mutex_lock(&manager->mutex);// 3. 检查关闭标志:如果线程池正在关闭,不能再添加新任务,避免任务泄漏if(manager->shutdown){pthread_mutex_unlock(&manager->mutex);return-2;}// 4. 将任务节点插入到任务队列尾部(FIFO 入队)sin_task->next=NULL;// 新任务成为新的尾部,next 置空if(manager->task_queue->tail)// 队列非空{manager->task_queue->tail->next=sin_task;// 当前尾节点指向新任务manager->task_queue->tail=sin_task;// 更新尾指针}else// 队列为空,新任务同时是头也是尾{manager->task_queue->head=sin_task;manager->task_queue->tail=sin_task;}// 5. 唤醒一个正在等待任务的工作线程// 注意:这里使用 signal 而不是 broadcast,因为只增加了一个任务,// 唤醒一个空闲线程即可,避免无效的“惊群效应”pthread_cond_signal(&manager->cond);// 6. 解锁:任务入队和唤醒操作已完成pthread_mutex_unlock(&manager->mutex);return0;}

2.5 线程池的销毁

线程池的销毁需要释放之前每个分批工作队列的空间,通过pthread_join销毁线程

intthreadpool_destroy(Manager*manager){if(!manager)return-1;pthread_mutex_lock(&manager->mutex);manager->shutdown=1;// Wake up all worker threads so they can drain remaining tasks and exit.pthread_cond_broadcast(&manager->cond);pthread_mutex_unlock(&manager->mutex);Worker*sin_worker=manager->worker_queue->head;while(sin_worker){Worker*worker_next=sin_worker->next;pthread_join(sin_worker->thread_id,NULL);free(sin_worker);sin_worker=worker_next;}pthread_mutex_destroy(&manager->mutex);pthread_cond_destroy(&manager->cond);free(manager->task_queue);free(manager->worker_queue);manager->task_queue=NULL;manager->worker_queue=NULL;return0;}