【学习笔记】从0理解线程池的实现

红烧奥特曼. 2026-04-08 18:42:22

 

一、前言

 

在学习多线程编程时,会有一些问题:
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

 

我的第一篇博客哈哈哈,从这里开始记录我的大学生活~

...全文
59 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

554

社区成员

发帖
与我相关
我的任务
社区描述
零声学院,目前拥有上千名C/C++开发者,我们致力将我们的学员组织起来,打造一个开发者学习交流技术的社区圈子。
nginx中间件后端 企业社区
社区管理员
  • Linux技术狂
  • Yttsam
  • 零声教育-晚晚
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

请新加入的VIP学员,先将自己参加活动的【所有文章】,同步至社区:

【内容管理】-【同步至社区-【零声开发者社区】

试试用AI创作助手写篇文章吧