固定线程池
提到线程池,通常说的都是固定大小的线程池,固定线程池的原理是这样的:
- 线程池由一个线程安全的队列,以及多个 worker 线程组成。
- 可以有多个 producer 线程,它们负责提交任务给线程池。
- 接收到新任务之后,线程池会唤醒某个 worker 线程,worker 线程醒来后会取出任务并执行。
虽然固定线程池实现起来很简单,但却有着几个缺陷:
- 无法动态扩容:worker 线程的个数是固定的,不能随着任务数的增长而增长。
- 无法动态缩容:如果有很多 worker 线程处于空闲状态,就会造成资源的浪费。
动态线程池
对于线程池,我们希望它可以动态增长,这样才不会造成任务队列的堆积,另一反面,也希望它能适当回收一些空闲的线程,以节省系统资源。
线程池接口
gRPC 内部使用了动态线程池,它的接口长这样:
那么DynamicThreadPool
是怎样管理线程的呢?
DynamicThreadPool
并不会限制线程的数量,理论上这意味着线程数量可以无限增长。- 构造函数接收一个参数
reserve_threads
,这个参数与线程池的缩容策略有关:它表示线程池最多只能有reserve_threads
个空闲线程。也就是说,如果线程池的空闲线程数量多于这个值,那么多出来的那些线程就会被系统回收。
线程池的构造
首先看看DynamicThreadPool
的数据成员:
DynamicThreadPool
的构造函数会先创建reserve_threads
个线程:
可以看到,线程池不会直接使用std::thread
,而是使用自己封装的DynamicThread
:
与std::thread
相比,DynamicThread
遵循了 RAII 的原则:DynamicThread
在析构时,会调用线程的join()
函数,用来确保正确地释放线程资源:
线程池的析构
DynamicThread
在退出之前,会将自己添加到线程池的dead_threads
中(在适当的时机,线程池会delete
掉dead_threads
中的所有线程,保证资源的释放)。
线程池的析构函数首先会唤醒所有休眠的线程,然后等待所有线程都退出,之后再调用reapThreads()
清理掉所有线程:
任务的提交与执行
线程池的Add()
函数用来提交任务,任务会被放到任务队列中,并唤醒一个空闲的线程去处理任务:
而ThreadFunc()
则展示了线程消费任务的逻辑:
完整的代码可以参照这里:grpc_dynamic_thread_pool