今天来写一个简单版本的线程池
1.啥是线程池
池塘,顾名思义,线程池就是一个有很多线程的容器。
我们只需要把任务交到这个线程的池子里面,其就能帮我们多线程执行任务,计算出结果。
与阻塞队列不同的是,线程池中内有一个队列用于任务管理,并帮我们封装了线程创建的工作。我们不再需要在主执行流里面创建线程(创建线程也是有时间消耗的),而是只关注于任务的创建,交给线程池来运行并产生结果就OK了
前面已经学习过阻塞队列了,此时再来写线程池,就没有那么困难了!
本次线程池的设计还会采用单例模式,同一个模板类型的任务,只需要一个线程池即可
1.1 简单复习单例模式
单例模式分为两种设计方式,一个是懒汉,一个是饿汉
- 懒汉:刚开始先不创建单例,等第一次使用的时候在创建;缺点是第一次获取单例需要等待,优点是程序启动快
- 饿汉:main函数执行前,就将单例创建起来;缺点是程序启动会比较慢,优点是启动之后获取单例会快
2.代码示例-处理task
2.1 成员变量
因为是线程池,需要在内部创建出线程来运行,所以我们需要一个num来标识需要创建的线程的数量
这里我们并不需要弄一个数组来存放已经创建的线程,因为我们并不关心线程的退出信息,也不需要对线程进行管理。在创建好线程之后,直接detach
即可
static变量我们需要在类外初始化,因为是模板类型,所以还需要带上template
关键字
2.2 构造/析构
本次使用的是懒汉模式的单例,提供一个指针作为单例,不开放构造函数
同时,利用delete
关键字,禁止拷贝构造和赋值重载;析构依旧保持公有
这种情况下,我们还需要有一个static成员函数来获取单例;在之前的单例模式博客中,提到当初实现的懒汉模式是线程不安全的,因为没有对线程进行加锁,避免多个执行流同时获取单例,导致单例对象冲突的问题。
现在学习了linux
的加锁操作,就可以避免掉这个bug了
两次nullptr判断
其中关于两次nullptr
判断的原因,详见注释
- 第一个判断是为了保证单例,只要单例存在了,就不再创建单例
- 第二个判断是保证线程安全,可能会出现线程a在创建单例,线程b在锁中等待的情况;此时如果不进行第二次nullptr判断,线程b从锁中被唤醒后,又会继续执行,多创建了一个单例!
2.3 启动线程池
有了线程池,接下来要做的就是启动它😁
启动之前,我们需要assert
判断一下该线程池是否已经启动了,避免多次启动线程池出现问题。启动完成之后,更新isStart
的状态值
这里还有另外一个函数threadRoutine
,这是每一个线程需要执行的函数,其为static函数。这里我们获取到的都是单例的this
指针,访问成员都需要通过this指针来访问
2.4 封装的加锁/解锁/通知操作
这部分操作比较简单,就不多提了。其实就是把已有的函数改个名字,变成无参可直接调用的函数罢了。
其中pop()
函数设置为了私有,因为线程池会自己开始处理任务,所以不需要外部pop
2.5 插入任务
最后就只剩下任务的插入了,插入一个任务后,使用条件变量,唤醒线程池中的一个线程来执行这个任务!
到这里,线程池就大功告成了!
3.测试
本次测试依旧使用了在线程博客中提到过的task.hpp
,完整代码详见 我的gitee仓库
因为使用了线程池,主执行流只需要来派发任务即可;
此时线程池就会帮我们运行,并将结果输出!
3.1 修改轻量级进程的名字
Linux提供了一个有趣的接口,可以允许我们修改轻量级进程的名字;
没有修改的时候,默认的名字都是该进程的可执行程序的名字
我们使用prctl
接口,修改名字;这个接口的作用是对一个进程进行操作。
其中修改线程名字的操作如下
分别修改主执行流和线程池中线程的名字,即可获得不一样的结果
这样可以用于标识线程的属性,还是有些用的!
The end
本篇博客到这里就over啦,有啥问题欢迎评论区提出哦!