线程池(Linux +C/C++)

参考  手写线程池 - C语言版 | 爱编程的大丙 (subingwen.cn)

1.为什么需要线程池?

1)线程问题:

(1)如果只使用线程创建函数,在不断有新的任务进来的时候,需要不断的创建任务;任务在结束之后,为了避免占用资源,需要销毁线程任务。导致频繁操作,占用程序运行时间。

(2)在线程创建之后,如果任务已经运行完毕,线程不去销毁,则会白白浪费资源。

2)如何解决线程问题(线程池的优势):

(1)使用线程池之后,将任务和线程分离,建立一定数量的线程,使线程重复利用(不销毁),不断将任务添加到线程中。解决线程频繁创建、销毁问题。提供系统执行效率。

(2)根据任务数量增减,自动添加或者减少线程,使得线程维持在最优数量,节约系统资源。

2.线程池是什么?

1)线程的基本组成:

(1)任务队列(负责保存要处理的任务,并将任务交给工作线程去处理)

(a)通过线程池提供的api函数,将待处理的任务添加到任务队列,或者从任务队列删除;

(b)已经处理的任务会被从任务队列删除;

(c)线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程

(2)工作者线程(任务队列的消费者、执行人员,动态创建N个)

(a)线程池中维护了一定数量的工作线程, 他们的作用是是不停的读任务队列, 从里边取出任务并处理;

(b)工作的线程相当于是任务队列的消费者角色;

(c)如果任务队列为空, 工作的线程将会被阻塞 (使用条件变量/信号量阻塞);

(d)如果阻塞之后有了新的任务, 由生产者将阻塞解除, 工作线程开始工作;

(3)管理者线程(管理整个线程池,1个)

(a)周期性地 对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测;

(b)根据任务数量的多少,增减线程数量;

3.线程池怎么实现?(linux/C语言版本)

1)单个任务元素结构体

// 任务结构体
typedef struct Task
{void (*function)(void* arg);void* arg;
}Task;

2)线程池结构体(任务队列+管理线程+工作线程)

// 线程池结构体struct ThreadPool
{Task* taskQ;        // 任务队列  后面利用堆新建数组int queueCapacity;  // 容量int queueSize;      // 当前任务个数int queueFront;     // 队头 -> 取数据int queueRear;      // 队尾 -> 放数据pthread_t managerID;    // 管理者线程IDpthread_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
};

3)API声明

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);

4)创建线程池

(1)主要操作:

(a)创建线程池对象:ThreadPool* pool = (ThreadPool*)malloc(sizeof(ThreadPool));

(b)创建工作者线程队列(pthread_t):只是为了便于管理线程,并没有创建线程。

        pool->threadIDs = (pthread_t*)malloc(sizeof(pthread_t) * max);

(c)创建任务队列(task类型):pool->taskQ = (Task*)malloc(sizeof(Task) * queueSize);

(d)创建管理者线程:pthread_create(&pool->managerID, NULL, manager, pool);

(e)创建工作者线程:按照最小数量minNum进行for循环;
           pthread_create(&pool->threadIDs[i], NULL, worker, pool);

(f)初始化互斥量/条件变量:mutexPool、mutexBusy、notEmpty、notFull

(g)其余工作:初始化一些控制变量

(h)鲁棒性工作:检查指针,及时释放内存

(2)说明:

(a)为什么使用do while ?可以使用break,最后统一销毁资源;

(b)memset,后续根据指针是不是0 判断是不是有线程闲置

ThreadPool* threadPoolCreate(int min, int max, int queueSize)
{//实例化线程池对象ThreadPool* pool = (ThreadPool*)malloc(sizeof(ThreadPool));do {//判断pool有没有指向有效内存if (pool == NULL){printf("malloc threadpool fail...\n");break;}//创建工作者线程队列,按照最多线程数量 创建pool->threadIDs = (pthread_t*)malloc(sizeof(pthread_t) * max);//判断threadIDs有没有指向有效内存if (pool->threadIDs == NULL){printf("malloc threadIDs fail...\n");break;}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);// 释放资源if (pool && pool->threadIDs) free(pool->threadIDs);if (pool && pool->taskQ) free(pool->taskQ);if (pool) free(pool);return NULL;
}

5)工作线程执行函数worker

(1)主要操作:

(a)判断任务队列是否为空。假如为空,阻塞线程。并进一步判断,是否要销毁线程池。

(b)如果任务队列不为空,取出任务,执行任务回调函数。

(2)说明:

(a)线程池属于公共资源,在访问线程池的时候要加锁;访问结束之后解锁。

(b)while (pool->queueSize == 0 && !pool->shutdown)  。在pthread_cond_wait这个地方,为什么采用while而不是if。关于该点,进行解释。

        首先,我们需要了解pthread_cond_wait的执行流程:

        -》pthread_cond_wait()函数会将当前线程挂起,使它处于休眠状态。
        -》在当前线程被挂起之前,函数会自动调用pthread_mutex_lock()函数将关联的互斥锁上锁,保证条件变量的独占访问。
        -》函数会将当前线程加入条件变量的等待队列中,并释放关联的互斥锁。
        -》当前线程进入休眠状态,等待其他线程在该条件变量上发出信号或广播。
        -》当另一个线程在该条件变量上发出信号或广播时,pthread_cond_wait()函数会唤醒一个等待在该条件变量上的线程。
        -》唤醒的线程重新获得关联的互斥锁的所有权,并继续执行。
        -》pthread_cond_wait()函数返回。

        其次,我们需要了解pthread_cond_signal的作用:至少唤醒一个线程。

        综合上面两点,设想以下场景。

初始临界变量x = 0;线程1:如果x< 1,阻塞。否则,继续执行,并设置x = 0;线程2:如果x< 1,阻塞。否则,继续执行,并设置x = 0;线程3:设置x = 1,调用pthread_cond_signal唤醒线程。

        情况1,采用if语句:线程1收到了线程唤醒,线程2收到了线程唤醒,线程1抢到了互斥锁,他正常进行,并把x设置为0;然后线程2因为收到了线程唤醒,他也开始执行。问题出现了,线程1不应该执行因为他的判断条件是 x<1的情况下,应该进行阻塞。可是他却开始执行了。

        情况2,采用while语句:线程1收到了线程唤醒,线程2收到了线程唤醒,线程1抢到了互斥锁,因为之前被阻塞了,while语句不满足,他又检查了一次while条件,发现不用阻塞了,他正常进行,并把x设置为0。然后线程2因为收到了线程唤醒,他也开始执行。因为之前也被阻塞了,while语句不满足,他有检查一次条件,发现x被修改了,x=0,需要被阻塞了。所以他没法往下执行了。所以接着被阻塞了。这种情况,就可以保证,条件变量只能唤醒一个线程。防止“伪唤醒”。

        回到本例子中,情况一样。线程池会唤醒多个工作线程,让他们去执行任务。但是有可能任务数<线程数。如果使用while循环,就能保证每个任务对应一个线程。如果使用if判断,有可能唤醒多余的线程,并引发程序崩溃。

void* worker(void* arg)
{ThreadPool* pool = (ThreadPool*)arg;while (1){pthread_mutex_lock(&pool->mutexPool);// 当前任务队列是否为空while (pool->queueSize == 0 && !pool->shutdown){// 阻塞工作线程pthread_cond_wait(&pool->notEmpty, &pool->mutexPool);// 判断是不是要销毁线程if (pool->exitNum > 0){pool->exitNum--;if (pool->liveNum > pool->minNum){pool->liveNum--;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--;//告诉生产者,可以添加任务了pthread_cond_signal(&pool->notFull);//解锁pthread_mutex_unlock(&pool->mutexPool);//工作线程数量+1printf("thread %ld start working...\n", pthread_self());pthread_mutex_lock(&pool->mutexBusy);pool->busyNum++;pthread_mutex_unlock(&pool->mutexBusy);//函数任务调用task.function(task.arg);//释放函数堆内存free(task.arg);task.arg = NULL;//工作线程数量-1printf("thread %ld end working...\n", pthread_self());pthread_mutex_lock(&pool->mutexBusy);pool->busyNum--;pthread_mutex_unlock(&pool->mutexBusy);}return NULL;
}

6)管理者任务

(1)主要操作:

(a)添加任务线程;

(b)销毁任务线程;

(2)说明:

销毁任务线程的思路:发出条件变量信号,唤醒所有的工作线程;并将shutdown参数,告诉工作线程,现在要关闭。让所有的线程自杀。

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){pthread_create(&pool->threadIDs[i], NULL, worker, pool);counter++;pool->liveNum++;}}pthread_mutex_unlock(&pool->mutexPool);}// 销毁线程// 忙的线程*2 < 存活的线程数 && 存活的线程>最小线程数if (busyNum * 2 < liveNum && liveNum > pool->minNum){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;
}

7)单线程退出函数

(1)主要操作:

前面判断线程是不是在工作,主要是判断线程ID是不是空。如果线程ID是空,表明为空线程;如果线程不为空,则为忙线程。因此,在退出删除线程的时候,需要修改线程队列中对应的线程ID,将其改为ID=0;便于后面的继续使用。

(2)说明

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);
}

8)线程池添加函数

(1)主要操作:

(a)如果线程池中的工作线程全部被占用,阻塞添加任务,即阻塞生产者;
(b)如果工作线程释放了notFull条件变量,则说明工作线程有空余,接触生产者

(2)说明:

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);
}

4.线程池怎么用?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/209619.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【已解决】为什么Word文档里有部分内容无法编辑?

小伙伴们是否遇到过这样的情况&#xff0c;打开Word文档进行编辑&#xff0c;发现部分内容可正常编辑&#xff0c;另外一部分内容却无法编辑。这是怎么回事&#xff0c;又要如何解决呢&#xff1f; 出现以上情况&#xff0c;一般是Word文档被设置了“限制保护”&#xff0c;使…

外包干了4年,技术退步太明显了。。。。。

先说一下自己的情况&#xff0c;本科生生&#xff0c;18年通过校招进入武汉某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年国庆&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测…

PLC通过485Modbus转Profinet网关与温控表通讯在发酵罐的应用

前提&#xff1a;在自动化控制系统中&#xff0c;PLC通常需要和各种设备进行数据通讯&#xff0c;其中就包括温控表。而这些设备之间的通讯常常需要通过485Modbus转Profinet网关&#xff08;XD-MDPN100&#xff09;来实现。 方案&#xff1a;在一些应用场合中&#xff0c;Profi…

【C语言】扫雷小游戏初学者版

成功的秘诀就是每天都比别人多努力一点。 今天给大家带来一款非常经典的小游戏——扫雷的实现和讲解 这里是目录 前言整体框架1.打印菜单2.创建二维数组3.初始化棋盘4.打印棋盘5.布置棋盘中的雷6.排查雷和统计雷总体代码test.cgame.cgame.h 进阶&#xff08;递归展开&#xff0…

OpenLayer库的学习入门总结

前言&#xff1a; 作者跟随视频学习ol库的调用与功能实现&#xff0c;进行初步总结与回顾。 声明&#xff1a;参考新中地的文档&#xff0c;进行作者日后复习再次学习的简化。 1、WebGIS简介 GIS的核心概念 GIS&#xff08;Geographic Information System&#xff09;是一…

项目demo —— GPT 聊天机器人

本文介绍我的开源项目 TelegramChatBot&#xff0c;这是一个基于 OpenAI GPT API 开发的 telegram 机器人&#xff0c;具有多模态交互能力&#xff0c;求 star&#xff01;感谢大家&#xff01;在 telegram jokerController_bot 立即体验&#xff01;欢迎对 GPT 应用开发或对 t…

[cocos creator]EditBox,editing-return事件,清空输入框

需求&#xff1a; 监听EditBox&#xff0c;editing-return 回车事件&#xff0c;在输入框内点击回车后&#xff0c;发送内容&#xff0c;并清空输入框 问题&#xff1a; 设置node.getComponent(EditBox).string ; 没有效果 解决办法&#xff1a; //设置string 为空 this.v…

Linux入门攻坚——7、磁盘管理——文件系统挂载管理及RAID、LVM

已经安装文件系统的分区需要经过挂载才能使用。 一切文件系统的使用都是从根开始&#xff0c;根是文件系统的起始点。 计算机启动过程&#xff1a;加电自检——bootloader——kernel——rootfs——/sbin/init kernel第一步要加载根系统。 将额外文件系统与根文件系统某现存的…

Selenium自动化测试:通过cookie绕过验证码的操作

验证码的处理 对于web应用&#xff0c;很多地方比如登录、发帖都需要输入验证码&#xff0c;类型也多种多样&#xff1b;登录/核心操作过程中&#xff0c;系统会产生随机的验证码图片&#xff0c;进行验证才能进行后续操作 ​解决验证码的方法如下&#xff1a; 1、开发做个万…

Ubuntu20.04使用SVN(Rabbitvcs)

原文&#xff1a;https://blog.csdn.net/u014552102/article/details/129914787 1.安装Rabbitvcs sudo apt-get install rabbitvcs-nautilus sudo reboot 安装完后&#xff0c;选中一个文件夹右键&#xff0c;即可看到相关操作&#xff0c;没有的可以重启一下。 2.添加这个p…

数字化转型失败率为什么这么高?

引言 数字化转型已成为当今商业环境中的重要议题。它指的是企业通过采用数字技术&#xff0c;对业务流程、文化和客户体验进行全面变革&#xff0c;以适应数字化时代的需求和趋势。 然而&#xff0c;尽管数字化转型的重要性日益凸显&#xff0c;但令人担忧的是&#xff0c;许多…

C++ 红黑树的封装

一.map/set的封装 在实现了红黑树的部分功能后&#xff0c;我们可以便可以将红黑树作为底层结构来封装map 和 set &#xff0c;但是问题也随之而来。我们都知道map是k-v的数据模型&#xff0c;而set是k的数据模型&#xff0c;我们难道要去使用两棵红黑树来封装吗&#xff1f;显…

【jupyter notebook中插件 nbextensions 安装失败分析与解决方法】

文章目录 问题描述分析与解决总结 问题描述 一开始在安装 notebook 中的插件 nbextensions 时根本没有注意到版本的适配问题&#xff0c;都是进行默认的安装&#xff0c;结果安装是最新版本的 notebook7.x&#xff0c;恰好 notebook7.x 版本不再适应插件 nbextensions&#xf…

降本增笑?滴滴史上最严重服务故障,裁员真不能裁测试

2023 年 11 月 27 日晚间&#xff0c;滴滴因系统故障导致 App 服务异常&#xff0c;不显示定位且无法打车。11 月 27 日晚&#xff0c;滴滴出行进行了回复&#xff1a;非常抱歉&#xff0c;由于系统故障。 2023 年 11 月 28 日早间&#xff0c;滴滴出行消息称&#xff0c;网约…

在高德地图SDK上加载五层十五级瓦片的方法

目录 前言实现思路加载高德SDK,显示地图加载GroundOverlay类加载五层十五级瓦片清除瓦片总结前言 因为项目需求,需要在高德地图上加载五层十五级瓦片。这八竿子打不着的结合,着实没有思路。好在高德地图SDK提供了一个加载地表覆盖物的接口(GroundOverlay),这就为加载五层…

antv x6填坑指南: 部分节点拖拽和操作撤销重做不生效问题、使用Stencil过滤时过滤后分组的显示高度无法根据过滤节点自适应问题

问题1. 部分分组中节点拖拽添加或操作后撤销重做操作不生效。 前提&#xff1a;使用Stencil插件&#xff0c;创建画布侧边栏的 UI 组件&#xff0c;同时使用其分组、折叠能力。分组包含固定分组、后台接口获取的动态分组和组件。 //固定分组初始化 initStencil (graph, stenc…

Ubuntu Server 20.04.6安装Anaconda3

下载安装包 去下面的网页找到自己想要安装的对应版本的链接&#xff1a; https://repo.anaconda.com/archive/ 我安装的版本链接如下&#xff1a; https://repo.anaconda.com/archive/Anaconda3-2023.09-0-Linux-x86_64.sh 复制这个链接后使用如下命令下载&#xff1a; wget …

国内的几款强大的AI智能—AI语言模型

R5Ai智能助手是一款由百度研发的文心一言&#xff0c;它支持gpt4 / gpt-3.5 / claude&#xff0c;也支持AI绘画&#xff0c;每天提供十次免费使用机会&#xff0c;无需魔法。该智能助手具有以下优点&#xff1a;会画画&#xff0c;没有使用次数限制&#xff0c;可以在界面上找到…

YOLOv8改进 | 2023 | 通过RFAConv重塑空间注意力(深度学习的前沿突破)

一、本文介绍 本文给大家带来的改进机制是RFAConv&#xff0c;全称为Receptive-Field Attention Convolution&#xff0c;是一种全新的空间注意力机制。与传统的空间注意力方法相比&#xff0c;RFAConv能够更有效地处理图像中的细节和复杂模式(适用于所有的检测对象都有一定的…