C/C++实现线程池、C++20信号量std::counting_semaphore

本文详细介绍了C语言实现的线程池,包括任务的存储结构、线程池的结构、工作线程与管理者线程的交互,以及如何使用C++改进线程池,使用`std::counting_semaphore`管理线程数量。作者展示了任务添加、线程动态调整和线程池销毁的过程。
摘要由CSDN通过智能技术生成

视频链接

C语言实现线程池

完整代码

任务的存储

  • 任务结构体
    任务是添加到任务队列的函数,因此需要设计一个结构体包含函数指针与函数参数

    //任务结构体
    typedef struct Task
    {
    	void (*function)(void* arg);//函数指针,返回值为void
    	void* arg;//函数的参数
    }Task;
    
  • 任务队列怎么实现
    直接使用2个变量记录队列头尾,手动实现队列。

线程池的实现

  • 为了管理者能够合理对线程数进行调整,需要记录当前当前存活的线程数与忙线程数

  • 为了各线程安全访问共享资源,如忙线程数量、当前存活线程数量,需要使用互斥锁,并使用条件变量实现生产者消费者模型。

    //线程池结构体
    struct ThreadPool
    {
    	Task* taskQ;//任务队列,由队头队尾下标维护成一个环形队列
    	int queueCapacity;//任务队列的容量
    	int queueSize;//当前任务个数
    	int queueFront;//队头下标
    	int queueRear;//队尾下标
    
    	pthread_t managerID;//管理者线程ID,管理者线程只有一个
    	pthread_t *threadIDs;//工作的线程ID,有多个工作线程
    	int minNum;//最小线程数
    	int maxNum;//最大线程数
    	int busyNum;//当前在工作的线程个数
    	int liveNum;//当前存活的线程个数
    	int exitNum;//如果大部分线程都闲着,需要杀死多少个线程
    
    	pthread_mutex_t mutexPool;//互斥锁,锁整个线程池
    	pthread_mutex_t mutexBusy;//单独用于锁busyNum,因为busyNum变化很频繁,如果每次都直接锁整个线程池的不太好
    
    	pthread_cond_t notFull;//条件变量,任务队列不满时唤醒生产者线程
    	pthread_cond_t notEmpty;//任务队列不为空时唤醒工作线程(消费者)
    
    	int shutdown;//要不要销毁线程池,销毁为1,不销毁为0
    };
    
  • 线程池初始化
    pthread_create创建出一个管理者线程、多个工作线程,并将线程ID存放在数组中。

    //创建线程
    pthread_create(&pool->managerID, NULL, manager, pool);//管理者线程
    //工作线程
    for (int i = 0; i < min; i++)
    {//工作线程的函数应该传入pool,到时候可以直接取到pool里面的任务队列taskQ和一些锁
    	pthread_create(&pool->threadIDs[i], NULL, worker, pool);
    }
    

工作线程与管理者线程

  • 工作线程(消费者)的函数
    循环读取任务队列,如果没任务了且线程池没关闭,那么就阻塞。否则,从头部取出一个任务来执行。
    为了管理者能让工作线程自杀,在工作线程阻塞被唤醒后,会对exitNum值进行判断,来决定是否线程自杀。

    //工作线程的函数
    void* worker(void* arg)
    {
    	......
    	//2.循环读取任务队列,而任务队列是共享资源,需要互斥访问
    	while (1)
    	{
    		pthread_mutex_lock(&pool->mutexPool);//mutexPool拿来专门锁整个线程池
    		//当前任务个数为0且线程池未被关闭
    		while (pool->queueSize == 0 && !pool->shutdown)
    		{
    			//那么应该阻塞工作线程
    			pthread_cond_wait(&pool->notEmpty, &pool->mutexPool);
    
    			//判当前需不需要销毁线程
    			if (pool->exitNum > 0)//在管理者发现闲着的线程过多时,会设置pool->exitNum值
    			{
    				//pthread_cond_wait在wait时自动释放锁,在被唤醒后又自动加锁了
    				//为了避免死锁,这里需要在线程自杀前释放锁
    				pool->exitNum--;//不管工作线程是否真的自杀,这里都应该--。否则,exitNum始终非0,所有被唤醒的线程都来自杀了
    				if (pool->liveNum > pool->minNum)
    				{
    					pool->liveNum--;//马上线程自杀了,活着的线程数应该-1
    					pthread_mutex_unlock(&pool->mutexPool);
    					threadExit(pool);//线程自杀
    				}
    				
    			}
    
    		}
    		
    		当前是正常的情况,工作线程进行消费
    		......
    		task.function(task.arg);
    		......
    
    	}
    	return NULL;
    }
    
  • 管理者线程的函数
    只要线程池没关闭,就每隔3秒检测一次,添加或减少工作线程数

    • 添加线程:任务个数 > 存活线程个数

      //添加线程:规定,当前任务个数>存活线程数 && 存活线程数<最大线程数,则添加线程
      if (queueSize > liveNum && liveNum < pool->maxNum)//maxNum是永远不会被改变的,因此无需互斥访问
      {
      	pthread_mutex_lock(&pool->mutexPool);//因为要操作pool->liveNum
      	int counter = 0;
      	//从存储线程id的数组中找到空闲位置 来存储新创建的线程id
      	for (int i = 0; i < pool->maxNum && counter < NUMBER && pool->liveNum < pool->maxNum; i++)
      	{
      		if (pool->threadIDs[i] == 0)//该位置的内存还没有存储线程id
      		{
      			pthread_create(&pool->threadIDs[i], NULL, worker, pool);//创建线程
      			counter++;
      			pool->liveNum++;
      		}
      	}
      	pthread_mutex_unlock(&pool->mutexPool);
      }
      
    • 销毁线程:设置exitNum值,再唤醒正在阻塞的线程,它们看到exitNum非0就自杀

      //销毁线程:规定,忙线程数*2<存活线程数 && 存活线程数>最小线程数
      if (busyNum*2<liveNum && liveNum>pool->minNum)
      {
      	pthread_mutex_lock(&pool->mutexPool);
      	pool->exitNum = NUMBER;
      	pthread_mutex_unlock(&pool->mutexPool);
      	//让工作线程自杀:唤醒wait的工作线程,工作线程会去检查pool->exitNum,非0则工作线程自杀
      	for (int i = 0; i < NUMBER; i++)
      	{
      		pthread_cond_signal(&pool->notEmpty);
      	}
      }
      

主线程

  • 主线程中添加任务

    int main()
    {
        //创建线程池
        ThreadPool* pool = threadPoolCreate(3, 10, 100);
        //添加任务
        for (int i = 0; i < 100; i++)
        {
            int* num = (int*)malloc(sizeof(int));
            *num = i + 100;
            threadPoolAdd(pool, taskFunc, num);
        }
        sleep(30);
        //销毁线程池
        thradPoolDestory(pool);
        return 0;
    }
    

改写成C++版线程池

完整代码

  • 在C语言版中,线程池与任务队列的实现混杂在一起。现在,将它们分开写成两个类ThreadPool和TaskQueue
  • 此前手动实现并管理队列,现在使用容器queue来自动管理。
  • 此前对线程池进行操作时需要将线程池对象传入,现在直接使用默认传过来的this指针即可获得线程池对象。
  • 使用模板来表明任务函数的类型

std::counting_semaphore<3> sema(0)

表示信号量最大值为3,初始值为0
acquire(): 如果信号量的内部计数大于0,那么这个方法会减少计数并立即返回。否则,这个方法会阻塞,直到其他线程调用release()方法增加了计数。
try_acquire(): 这个方法尝试减少信号量的计数。如果计数大于0,那么这个方法会减少计数并返回true。否则,这个方法不会阻塞,而是立即返回false。
release(n = 1): 这个方法增加信号量的计数。参数n指定了要增加的数量,默认为1。如果有其他线程在等待信号量(即它们调用了acquire()方法并被阻塞),那么这个方法会唤醒那些线程。

  • 32
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
学习并掌握C++2.0(11+14+17+20)的新特性,学习线程及线程池的应用 ---------------------------------------------------给小白学员的3年学习路径及计划技术方面分三块:1.纯开发技术方向2.音视频流媒体专业方向3.项目实战---------------------------------------------------1.纯开发技术方向(1) C++必须要过硬(至少学会10本经典好书)(2) 系统级编程(Windows、Linux),必须特别熟练系统API,灵活运用(3) 框架与工具(Qt、MFC):必须精通其中一种。(4) 架构与设计模式:需要提升一个高度,不再是简单的编码,而是思维模式。(5) 驱动级别(如果有兴趣,可以深入到驱动级:包括Windows、Linux)(6) 最好学习点Java+Html+javascript等WEB技术。2.音视频流媒体专业方向(1) 音视频流媒体基础理论:   必须认真学会,否则看代码就是看天书(2) 编解码方向:精通h.264,h.265(hevc), 包括理论和各个开源库(ffmpeg,libx264,libx265,...)。(3) 直播方向:  精通各种直播协议(rtsp,rtmp,hls,http-flv,...), 钻研各个开源库(live555,darwin,srs,zlmediakit,crtmpserver,...)(4) 视频监控:  理论+开源库(onvif+281818)(EasyMonitor、iSpy、ZoneMinder(web)、...) 3.项目实战(1) Qt项目:  至少要亲手练习10个实战项目(网络服务器、多线程、数据库、图像处理、多人聊天、等等)(2)音视频项目:包括编解码、视频监控、直播等各个方向,都需要亲手实战项目,包括视频服务器、后台管理系统、前端播放器(多端)---------------------------------------------------  第1章 C++11新特性 41). nullptr关键字与新语法 42). auto和decltype类型推导 6 auto讲解 6 auto示例 7 decltype 83). for区间迭代 94). 初始化列表 105). 模板增强 11外部模板 11类型别名模板 12默认模板参数 126). 构造函数 13委托构造 13继承构造 147). Lambda 表达式 158). 新增容器 20std::array 20std::forward_list 21无序容器 22元组 std::tuple 239). 正则表达式 2610). 语言级线程支持 28多线程库简介 2811). 右值引用和move语义 31右值引用和move语义 32转移左值 3412). constexpr 35第2章 C++14新特性 36Lambda 函数 36类型推导 37返回值类型推导(Return type deduction) 37泛型lambda 39[[弃用的]]  [[deprecated]]属性 40二进制数字和数字分隔符 41第3章 C++17新特性 42安装GCC10.2 42安装msys2-x86_64-20200720 42更新镜像 42更新软件库 43安装 MinGW64 等必要的软件 43环境变量Path 43编译命令 43constexpr 44typename 45折叠表达式 47结构化绑定 48条件分支语句初始化 49聚合初始化 50嵌套命名空间 52lambda表达式捕获*this的值 53改写/继承构造函数 54用auto作为非类型模板参数 55__has_include 56fallthrough 57nodiscard 57maybe_unused 58第4章 C++20新特性 59编译命令 59concept 59typename 60explicit 61constinit 62位域变量的默认成员初始化 62指定初始化 63基于范围的for循环初始化 64放宽基于范围的for循环,新增自定义范围方法 65嵌套内联命名空间 66允许用圆括弧的值进行聚合初始化 67unicode字符串字面量 68允许转换成未知边界的数组 68likely和unlikely 69第5章 C++2.0(11/14/17/20)总结与分析 705.1 C语言C++ 715.2 语言可用性的强化 725.2.1 常量 725.2.2 变量及其初始化 735.2.3 类型推导 745.2.4 控制流 765.2.5 模板 775.2.6 面向对象 815.3 语言运行期的强化 835.3.1 Lambda 表达式 835.3.2 右值引用 865.4 容器 885.4.1 线性容器 885.4.2 无序容器 895.4.3 元组 895.5 智能指针与内存管理 905.5.1 RAII 与引用计数 905.5.2 std::shared_ptr 905.5.3 std::unique_ptr 915.5.4 std::weak_ptr 91第6章 C++2.0多线程原理与实战 93什么是并发 93并发的方式 93为什么使用并发 95线程简介 96创建线程的三种方式 971. 通过函数 972.通过类对象创建线程 993.通过lambda表达式创建线程 101thread线程的使用 101互斥量与临界区 105期物Future 111条件变量 112原子操作 114内存模型 118第7章 C++2.0线程池原理与实战 120线程与线程池的基本原理 1201)、线程 1202)、线程的生命周期 1213)、什么是单线程和多线程 1214)、线程池 1225)、四种常见的线程池 123线程池的架构与流程 123线程池代码实战 125    
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值