线程池实现学习笔记1

news/2025/3/26 3:52:53/

线程池实现学习笔记

今天花了一些时间学习和实现了线程池,收获颇丰。在这里记录一下自己的学习心得,希望对大家也有帮助。

为什么需要线程池?

在实际开发中,如果每个任务都创建一个新线程,当任务数量很大时会带来以下问题:

  1. 线程创建和销毁的开销大

    • 创建线程需要分配内存空间
    • 需要初始化线程栈(默认大小通常为1MB)
    • 需要进行系统调用创建内核线程
    • 销毁线程需要回收资源,也会消耗系统资源
  2. 系统资源占用过多

    • 每个线程都占用一定的内存空间
    • 线程过多会导致系统内存紧张
    • 线程的上下文信息也会占用内核空间
  3. 线程数量过多导致系统不稳定

    • 超出系统支持的最大线程数限制
    • 过多的线程竞争CPU资源
    • 可能导致系统响应变慢
  4. 线程频繁切换导致CPU开销增大

    • 线程切换需要保存和恢复上下文
    • 频繁的上下文切换会降低CPU利用率
    • 影响程序的整体性能

这时候就需要线程池来解决这些问题。线程池通过复用已创建的线程来执行任务,避免了频繁创建和销毁线程的开销。

线程池的核心数据结构

首先,我们需要定义线程池的基本结构。每个结构体的设计都有其特定的用途:

typedef struct threadpool_t {pthread_t *thread;         // 线程数组,存储线程池中的线程int threadd_nums;         // 线程数量,表示池中的线程数task_queue_t que;        // 任务队列,存储待执行的任务pthread_mutex_t mutex;    // 互斥锁,用于保护共享资源pthread_cond_t cond;     // 条件变量,用于线程间的通知机制bool is_running;         // 线程池运行状态,控制线程池的生命周期
} threadpool_t;// 任务结构体:定义任务的基本单位
typedef struct task_t {void (*function)(void* arg);  // 函数指针,指向任务函数void* arg;                    // 函数参数,可以传递任意类型的数据
} task_t;// 任务队列结构体:实现任务的FIFO管理
typedef struct task_queue_t {task_t* tasks;           // 任务数组,存储队列中的任务int capacity;           // 队列容量,表示最大可以存储的任务数int size;              // 当前任务数量,表示队列中实际的任务数int front;             // 队首索引,指向第一个任务int rear;              // 队尾索引,指向下一个可以插入任务的位置
} task_queue_t;

详细实现过程

1. 任务队列的实现

任务队列采用循环队列的设计,这样可以高效地利用空间:

// 初始化队列:分配内存并初始化各项参数
void queueInit(task_queue_t* queue) {queue->capacity = QUEUE_SIZE;    // 设置队列容量queue->size = 0;                 // 初始化任务数量为0queue->front = 0;                // 队首指向0queue->rear = 0;                 // 队尾指向0// 分配任务数组内存queue->tasks = (task_t*)malloc(sizeof(task_t) * QUEUE_SIZE);
}// 添加任务:将任务添加到队列尾部
bool queuePush(task_queue_t* queue, task_t task) {// 检查队列是否已满if (queue->size >= queue->capacity) {return false;}// 将任务添加到队尾queue->tasks[queue->rear] = task;// 更新队尾位置,使用取模运算实现循环queue->rear = (queue->rear + 1) % queue->capacity;queue->size++;return true;
}// 获取任务:从队列头部取出任务
bool queuePop(task_queue_t* queue, task_t* task) {// 检查队列是否为空if (queue->size <= 0) {return false;}// 取出队首任务*task = queue->tasks[queue->front];// 更新队首位置,使用取模运算实现循环queue->front = (queue->front + 1) % queue->capacity;queue->size--;return true;
}

2. 工作线程函数实现

工作线程的实现体现了线程池的核心工作原理:

void* threadFunc(void* arg) {threadpool_t* pool = (threadpool_t*)arg;task_t task;while (pool->is_running) {  // 线程池运行状态检查pthread_mutex_lock(&pool->mutex);  // 获取互斥锁// 使用while循环是为了防止虚假唤醒while (pool->que.size == 0 && pool->is_running) {// 当队列为空时,线程等待条件变量通知pthread_cond_wait(&pool->cond, &pool->mutex);}// 再次检查运行状态,防止线程池已关闭if (!pool->is_running) {pthread_mutex_unlock(&pool->mutex);break;}// 尝试获取任务if (queuePop(&pool->que, &task)) {pthread_mutex_unlock(&pool->mutex);  // 解锁,让其他线程能够访问队列task.function(task.arg);  // 执行任务} else {pthread_mutex_unlock(&pool->mutex);}}return NULL;
}

3. 线程池的完整生命周期

初始化
void threadpoolInit(threadpool_t *pool, int num) {if (pool) {// 分配线程数组内存,使用calloc自动初始化为0pool->thread = (pthread_t *)calloc(num, sizeof(pthread_t *));pool->threadd_nums = num;  // 设置线程数量pool->is_running = true;   // 设置运行状态为true// 初始化同步原语pthread_mutex_init(&pool->mutex, NULL);  // 初始化互斥锁pthread_cond_init(&pool->cond, NULL);    // 初始化条件变量queueInit(&pool->que);                   // 初始化任务队列}
}
启动线程池
void threadpoolStart(threadpool_t *pool) {// 创建指定数量的工作线程for (int i = 0; i < pool->threadd_nums; i++) {// 创建线程,并将线程池指针作为参数传递int ret = pthread_create(&pool->thread[i], NULL, threadFunc, pool);if (ret != 0) {// 错误处理:打印错误信息并退出fprintf(stderr, "pthread_create failed: %s\n", strerror(ret));exit(1);}}
}
添加任务
void threadpoolAddTask(threadpool_t* pool, task_t task) {pthread_mutex_lock(&pool->mutex);  // 获取互斥锁// 将任务添加到队列if (queuePush(&pool->que, task)) {// 成功添加任务后,唤醒一个等待的线程pthread_cond_signal(&pool->cond);}pthread_mutex_unlock(&pool->mutex);  // 释放互斥锁
}
销毁线程池
void threadpoolDestroy(threadpool_t *pool) {if (pool) {// 设置停止标志,通知所有线程准备退出pool->is_running = false;// 唤醒所有等待的线程,使其检查运行状态并退出pthread_cond_broadcast(&pool->cond);// 等待所有线程完成当前任务并退出for (int i = 0; i < pool->threadd_nums; i++) {pthread_join(pool->thread[i], NULL);}// 按顺序释放所有资源free(pool->thread);              // 释放线程数组queueDestroy(&pool->que);        // 释放任务队列pthread_mutex_destroy(&pool->mutex);  // 销毁互斥锁pthread_cond_destroy(&pool->cond);    // 销毁条件变量}
}

实际使用示例

下面是一个完整的使用示例,展示了线程池的基本用法:

// 任务函数:打印任务编号并模拟耗时操作
void taskFunc(void* arg) {int num = *(int*)arg;printf("Task %d is running\n", num);sleep(1);  // 模拟任务执行时间free(arg); // 释放参数内存
}int main() {threadpool_t pool;// 初始化线程池,创建4个工作线程threadpoolInit(&pool, 4);threadpoolStart(&pool);// 添加10个任务到线程池for (int i = 0; i < 10; i++) {// 为每个任务分配参数内存int* arg = malloc(sizeof(int));*arg = i;// 创建任务并添加到线程池task_t task = {taskFunc, arg};threadpoolAddTask(&pool, task);}sleep(5);  // 等待任务执行完成threadpoolDestroy(&pool);  // 销毁线程池return 0;
}

遇到的问题和解决方案

在实现过程中,我遇到了一些典型的并发编程问题:

  1. 内存管理问题

    • 任务参数的内存泄漏:
      • 问题:任务参数是动态分配的,如果不及时释放会造成内存泄漏
      • 解决:在任务函数执行完成后释放参数内存
    • 线程池资源的释放顺序:
      • 问题:资源释放顺序不当可能导致访问已释放的内存
      • 解决:先停止所有线程,再按照依赖关系顺序释放资源
  2. 线程安全问题

    • 任务队列的并发访问:
      • 问题:多个线程同时操作队列可能导致数据竞争
      • 解决:使用互斥锁保护队列的所有操作
    • 条件变量的使用:
      • 问题:可能发生虚假唤醒,导致线程在队列为空时仍被唤醒
      • 解决:使用while循环检查条件,配合互斥锁使用
  3. 死锁问题

    • 在获取任务时可能发生死锁:
      • 问题:锁的获取和释放顺序不当
      • 解决:保证所有线程按相同顺序获取锁,及时释放锁
    • 销毁时的死锁:
      • 问题:线程池销毁时,线程可能还在等待条件变量
      • 解决:先设置停止标志,再唤醒所有等待的线程

性能优化

  1. 任务队列优化

    • 使用无锁队列提高并发性能:
      • CAS操作替代互斥锁
      • 使用内存屏障保证内存顺序
    • 实现动态扩容的任务队列:
      • 监控队列使用率
      • 适时调整队列容量
  2. 线程管理优化

    • 实现动态线程数量调整:
      • 根据任务量动态增减线程
      • 设置合理的线程数阈值
    • 添加线程池负载监控:
      • 记录任务执行时间
      • 统计线程利用率

后续优化方向

  1. 添加线程池状态管理

    • 运行状态监控:
      • 记录线程池运行状态
      • 提供状态查询接口
    • 任务执行统计:
      • 统计任务执行次数和时间
      • 生成性能报告
  2. 实现动态扩缩容

    • 基于任务量自动调整线程数:
      • 设置任务队列水位线
      • 根据水位线调整线程数
    • 设置最大最小线程数限制:
      • 防止线程数过多或过少
      • 保证系统稳定性
  3. 添加任务优先级支持

    • 实现优先级队列:
      • 多级任务队列
      • 优先级调度算法
    • 支持任务取消和超时:
      • 任务状态管理
      • 超时处理机制
  4. 完善错误处理机制

    • 异常处理和恢复:
      • 捕获并处理异常
      • 实现自动恢复机制
    • 日志记录系统:
      • 记录关键操作日志
      • 支持不同级别的日志

这个实现虽然还有优化空间,但已经包含了线程池的核心功能。在实际项目中,我们可以根据具体需求进行扩展和改进。

希望这篇文章能帮助到同样在学习线程池的朋友们。如果有任何问题或建议,欢迎在评论区讨论!


http://www.ppmy.cn/news/1583124.html

相关文章

SQL Optimization

SQL Optimization &#xff08;SQL 优化&#xff09; 1) * && field SELECT * from sys_user SELECT USER_ID, USER_NAME, EMAIL FROM SYS_USER; 栗子&#xff1a; 48.664s 142877rows 6.194s 142877rows 2&#xff09;UNION && UNION ALL …

单链表的查找和插入,删除操作

1.单链表的查找 snode* slistfind(snode* stlheap, stltype x) {while (stlheap){if (stlheap->data x){return stlheap;}stlheap stlheap->next;}return NULL; } 2.单链表的插入操作 2.1在指定位置之前插入节点 void slistinsert(snode** stlheap, snode* pos, stl…

Linux:基础IO---文件描述符

文章目录 1. 前言1.1 C语言文件知识回顾 2. 文件2.1 文件基础知识 3. 被打开的文件3.1 以C语言为主&#xff0c;先回忆一下C文件接口3.2 过渡到系统&#xff0c;认识文件系统调用3.3 访问文件的本质3.4 重定向&&缓冲区 序&#xff1a;在深入了解了进程的内容后&#xf…

游戏引擎学习第172天

总结今天的计划 这次的项目我们没有使用任何游戏引擎或者第三方库&#xff0c;而是完全自己动手编写。这种方式可能没有经济效益&#xff0c;但我认为每个人都应该有一次亲身经历&#xff0c;了解开发一款游戏时所涉及的所有内容。这样能让开发者更加灵活&#xff0c;能够做很…

数学建模:MATLAB卷积神经网络

一、简述 卷积神经网络是一种处理具有网格结构数据的深度学习模型&#xff0c;由输入层、卷积层、池化层、全连接层、输出层组成。 输出层&#xff1a;将图像转换为其对应的由像素值构成的二维矩阵&#xff0c;并存储二维矩阵 卷积层&#xff1a;提取图像的底层特征&#xf…

智慧医院、养老人员高精度定位解决方案

随着医院就医人数的不断增加&#xff0c;人员管理方面出现了诸多漏洞&#xff0c;表现为患者私自走出病房甚至医院;新生婴儿的有效管理&#xff0c;患者遇到突发病情&#xff0c;得不到及时救助&#xff0c;医疗设备看管措施不够严密&#xff0c;出现丢失等。 品铂科技高精度定…

Windows下rust的安装

前言 Python在编译某些包的时候需要用到rust&#xff0c;一怒之下就打算直接将rust安装上。我采用的平台是Windows。 一、登录rust官网 https://rustup.rs/ 二、安装 下载完毕后&#xff0c;直接运行&#xff0c;呈现如下画面&#xff1a; 回车&#xff0c;继续即可。(打算简…

【AndroidRTC-11】如何理解webrtc的Source、TrackSink

Android-RTC系列软重启&#xff0c;改变以往细读源代码的方式 改为 带上实际问题分析代码。增加实用性&#xff0c;方便形成肌肉记忆。同时不分种类、不分难易程度&#xff0c;在线征集问题切入点。 问题1&#xff1a;如何理解VideoSource、VideoTrack&VideoSink三者的关系…