线程池
介绍
线程池: 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量
线程池的价值:
- 需要大量的线程来完成任务,且完成任务的时间比较短。可同时处理多任务,多请求。
- 有任务可以立即从线程池中调取线程取处理,节省了线程创建的时间
- 有效防止服务端线程过多而导致系统过载的问题
实现
线程池中首先需要有很多个线程,用户可以自己选择创建多少个线程。为了实现线程间的同步与互斥,还需要增加两个变量——互斥量和条件变量。我们还需要一个任务队列,主线程不断往里面塞任务,线程池的线程不断去处理。需要注意的是:这里的任务队列可以为空,但不能满,所以任务队列的容量不限定(实际场景中,任务队列容量不够就需要考虑换一台更大的服务器)
线程池的四个成员变量:
- 一个队列: 存放任务
- 线程池中线程数: 记录线程池中创建的线程数
- 互斥量: 一个互斥锁
- 条件变量: 两个条件变量
线程池:首先需要创建几个线程,还有一个任务队列,当任务队列有任务的时候就唤醒一个正在等待的线程,让线程去执行任务,线程池中的线程执行完任务不会销毁,大大减少的cpu的消耗。
需要两个条件变量和一个互斥锁,这个互斥锁用来锁住任务队列,因为任务队列是公共资源,其次还需要两个条件变量,一个条件变量用来阻塞取任务的线程,当队列中有任务的时候,直接取任务,然后解锁,当任务队列中没有任务的时候,解锁等待条件,条件满足抢锁,取任务,解锁。另一个条件变量用来阻塞添加者进程,当任务队列满了,会让添加者进程等待,当有线程取走一个任务的时候,会唤醒添加者进程。
版本一
任务函数
这里的任务函数采用的时回调函数的方式,提高了代码的通用性,可以根据自己的需求改写任务函数
线程池的主要代码框架
放任务: 主线程无脑往任务队列中塞任务,塞任务之前进行加锁,塞完任务解锁,如果任务队列已经满了,等待线程取任务,然后唤醒在条件变量下等待的队列;放入了任务就给线程发送信号,唤醒线程来取 取任务: 线程池中的线程从任务队列中取任务,需要对任务队列上锁,因为对公共资源的操作都需要上锁,如果没有任务就阻塞,等待放任务唤醒;如果取完了一个任务,就唤醒添加任务
这就是两个条件变量和一个互斥锁的用法
整体代码:
运行结果如下:
可以看到线程最多处理三个任务,而任务队列中最多可以存在20个任务,当线程取走了任务之后,唤醒生产者继续添加任务。
版本二
首先封装一个任务:
线程池的主要代码框架(唤醒和等待操作都已经封装好):
创建多个线程 创建多个线程可以用一个循环进行创建。需要注意的是,创建一个线程还需要提供一个线程启动后要执行的函数,这个启动函数只能有一个参数。如果把这个函数设置为成员函数,那么这个函数的第一个参数默认是this指针,这样显然是不可行的,所以这里我们考虑把这个启动函数设置为静态的。但是设置为静态的成员函数又会面临一个问题:如何调用其他成员函数和成员变量? 所以这里我们考虑创建线程的时候,把this指针传过去,让启动函数的arg 参数去接收即可
注意: 线程创建后,执行启动函数,在这个函数中,线程会去任务队列中取任务并处理,取任务前需要进行加锁的操作(如果队列为空需要挂起等待),取完任务然后进行解锁,然后处理任务,让其它线程去任务队列中取任务
放任务: 主线程无脑往任务队列中塞任务,塞任务之前进行加锁,塞完任务解锁,然后唤醒在条件变量下等待的队列 取任务: 线程池中的线程从任务队列中取任务,这里不需要加锁,因为这个动作在启动函数中加锁的那一段区间中被调用的,其实已经上锁了
这两个版本都可以实现简易的线程池,下面线程池版本的服务器主要是用版本二来实现,因为版本一要修改的内容有点多,小伙伴们可以自己修改一下
线程池版本服务器
多线程版本效果看起来还不错,但是来一个连接就创建一个线程,断开一个连接就释放一个线程,这样频繁地创建和释放线程资源,对OS来说是一种负担,同时也带来资源的浪费,如果我们使用线程池,把每一个客户端连接封装成一个任务,让线程池去处理,这样就不需要频繁地创建和销毁消除,效率也能提升很多。 线程池采用版本二,代码如下:
这里我们单独写一个头文件——Task.hpp,其中有任务类,任务类里面有三个成员变量,也就是端口号,IP和套接字,其中有一个成员方法——Run,里面封装了一个Service函数,也就是前面写的,把它放在Task.hpp这个头文件下,线程池里面的线程执行run函数即可,头文件内容如下:
服务器类的核心代码如下:
注意几点变化:
- 服务器类增加一个线程池成员变量,初始化函数里面增加线程池创建(在堆上申请)
- 析构函数增加释放线程池资源一步
- loop函数中只需要封装任务,并把任务丢进线程池中即可
标签:
留言评论