线程池

9 阅读6分钟

设计线程池的原因

我们为什么要设计线程池这样一个容器?

因为线程本质也是一种资源,是一个执行任务的工人,他自己不拥有资源,仅仅只是被调度去干活.而线程池的创建/销毁是一件非常耗费资源的事情.

  • 线程的资源分配

    • 分配线程栈
    • 创建内核线程对象
    • 初始化线程本地存储TLS,调度信息,信号掩码
    • 调度器把线程加入可运行队列
  • 固定线程数量的重要性:

    • 如果来一个连接请求就开始pthread_create,那么高峰期的话CPU可能一直在造线程
    • 这样还会导致线程数量暴涨,然后直接把进程的栈区的空间给挤爆了,因为一直创建线程就得一直从栈区给新线程分配线程栈,高峰期如果数量过多的话线程栈直接爆了
    • 如果我们固定好线程的数量,这样的话来了新的连接也是在任务队列里面排队,不会无限制的创建新的线程,然后现有的线程根据队列的排列顺序从里面取出任务并执行,是一个比较合理切实可行的方案.
    • 线程数量固定在一个合理范围内也能让CPU对上下文切换保持可控
  • 线程生命周期的统一管理

    • 线程生命周期阶段 --- 1. 创建 2. 运行 3. 阻塞/就绪 4. 销毁

    • 由此我们可以去思考责任归属

      • 线程创建由谁负责?
      • 线程销毁由谁负责?
      • 出错/关闭的时候如何去处理线程,由谁去处理线程?

于是我们发现,如果没有一个集中的容器来统一管理工人线程的处于不同的状态在不同的阶段执行不同的任务的时候,是一件很不方便也不合理的事情,会增加整个代码设计的耦合度,为了让多线程像一个可控的单线程一样可以预测它的行为和更改它的属性,所以线程池应运而生了,可以在线程池里面添加你想要知道的线程的一切属性,由程序员本身来设计线程池.

线程池的好处 :

  • 把线程从一次性消耗品变成了可复用的执行资源,降低了创建/销毁的系统成本,创建了N个工人线程之后,后续任务就只有 1. 任务入队 2. 唤醒 3. 工人线程取任务执行
  • 固定线程数量,避免资源爆炸 线程池可以把并发度控制在一个合理的范围,任务数量增长等于排队增长,而不是线程数量增长
  • 统一线程生命周期管理 简单的说就是把线程本身的一些与业务无关的行为从业务逻辑里面给分离出去,让这些责任(创建,销毁,退出,数量,停止,任务分配等等)的负责人变成了线程池这个容器,然后我们的业务代码就不用关心线程的一个问题,只需要注重于 任务本身和具体的业务流程,从而实现解耦的目的.

线程池和线程以及任务的关系

三个对象:

  • 线程池

  • 线程

  • 任务队列

    • 线程池 :线程池里面包括了线程和任务队列 任务进入线程池的任务队列,线程池里的线程从线程池里的任务队列里面消费任务
    • 任务 : **任务和线程没有半毛钱关系 ! **都是通过线程池联系起来的
    • 任务队列 : 任务队列与线程之间没有静态绑定关系,它们只通过线程池的调度在运行时产生短暂联系

    补充: 工作线程必须要拿到ThreadPool

    工作线程执行的其实是线程池的一个调度协议,而不是业务逻辑,具体业务逻辑是写在任务里面的,所以工作线程只需要遵循线程池的调度从任务队列里面去拿任务就行,也不关系任务具体是什么.

    然后它还需要知道一些信息,就是线程池里面的一些属性,通过这些属性来判断自己是不是要继续执行工作,从哪里拿任务,是不是要继续或者,这样的话就需要那个ThreadPool 指针

总结: 线程池的核心职责是调度与管理,而不是执行具体业务 线程只是执行载体,任务才是业务逻辑的承载者


线程池的设计与组成原理

以每个对象都有自己负责的一部分职责,不关心其它对象的所负责的事情,达到干净的对象边界.从而把一件复杂的事情解耦,这个其实有点类似于C++里面的面向对象编程.

  1. 工人线程集合

  2. 任务队列

  3. 同步和控制组件

    1. 同步组件: a. 条件变量 b. 互斥锁 c. 信号量
    2. 控制组件: 负责生命周期管理 一些状态变量等等
为什么很多线程池代码里,同步和控制字段都塞在 ThreadPool 结构体里?

ThreadPool 本身就是“控制对象” worker / queue 都是被管理对象


线程池中的线程的设计与组成原理

工人线程归属于线程池,所以工人线程中需要一个指向线程池的指针,用来查看一些同步和控制组件,好规范自己行为,以及自己的生命周期

  • worker 必须在同一把锁/同一套条件变量规则下从队列取任务(避免竞态)
  • worker 必须在 shutdown 时遵守统一退出协议(避免僵死线程/资源泄露)

工人线程还需要知道任务队列,还有同步原语的信息,而任务队列,同步原语都归属于线程池

线程池里面含有一些对于工人线程的同步和控制字段,比如说统计一下正在工作的线程,正在睡觉的线程的工人线程的数量,单个工人线程只知道自己在不在工作,但是线程池需要知道所有工人线程的不同状态的综合,用来判断CPU的负载.这些的话都需要去线程池里面的字段改,所以需要一个指向线程池的指针.