本文用于笔者对在开发 Ngina - server 服务器框架过程中,曾设计过的线程池任务调度进行相关分析,主要包括线程池设计思路、线程池实现原理和应用进行详细阐述,希望可以帮助读者更好的理解线程池这一设计理念。
线程池
,笔者习惯将其理解为一种,将线程池化的技术,众所周知,多线程是高并发的重要实现方式之一,可以极大提高执行任务的效率,但是同时多线程的引入也会带来一些潜在的问题,比如如何安全的创建、管理、和调度线程执行任务?因此,线程池的设想便应运而生。
线程池,是一种组织多线程的形式,通过在初始化阶段一次性创建足够数量的线程,并将这些线程统一保存在指定容器当中,等待有任务到来时,进行调度。简单来讲,可以理解为,有一个很大的池子,里面“养”着很多线程,当需要执行任务时,便从池子中唤醒一个线程去处理。这样的好处是可以避免线程不断创建、销毁带来的开销和不确定性,线程得以复用。
除了线程池之外,还有内存池、连接池等其他池化技术,应用范围十分广泛。
相比于传统的,不断创建、销毁线程的方式,池化技术的引入带来了很多便捷之处。
控制资源
,预先设定线程数量上限,可以避免 QPS 过高导致服务器资源耗尽、线程创建失败产生的一系列问题。
线程复用
,反复的创建和销毁都需要一定的时空开销,因此线程池可以在一定程度上提升程序性能。
当然,线程池也有他的缺点,如线程池的数量配置需要合理,线程间通信、共享数据保护等。
核心线程数
,是线程池中常住的线程数量,一般来讲线程池中的线程数不应当少于此数值,用于在大多数情况下处理任务,在任务不多的情况下会被阻塞。
最大线程数
,除了核心线程之外,会在必要情况下创建一定数量的线程解决短时间内到达的大量任务,这部分线程被创建出后一般会立刻投入使用,处理任务结束后会被回收,等待销毁。
任务队列,即存放待处理任务的队列,队列组织方式大致有以下几种:
ArrayBlockingQueue
,是由数组实现的有界的阻塞队列,在初始化的时候,必须指定大小。
LinkedBlockingQueue
,是由链表实现的无界的阻塞队列,可以在初始化时指定大小。
DelayQueue
,延迟队列,只有延迟期满足才会从队列中获取元素。
SynchronousQueue
,是一个不存储元素的阻塞队列:若是插入时,已经有一个元素,就会阻塞等待,直到这个元素被移除,反之亦然。
LinkedBlockingDeque
,是一个由链表组成的双向阻塞队列。
一般来讲,线程数的设置需要考虑多方面的因素,综合决定。常见的有两种方式:
IO密集型任务
:这类型任务输入输出会更多一些,不是一直在执行任务,因此线程数可以设置的略大一些,参考公式为 2 * Ncpu。
CPU密集型任务
:这类任务会使得 CPU 被频繁占用,因此对于这类任务,线程数应该尽量少,参考的公式为:Ncpu + 1。
但是实际上,一般是结合压测来进行设置,先预先设置一个比较大的线程数,然后进行压测,通过监控CPU和内存的变化来修改线程数。
线程池中线程数存在上限,因此,任务队列中不可避免的会堆积任务,当任务数量超出队列上限后如何处理?这就涉及到线程池的拒绝策略了,大致有以下几种:
DiscardPolicy
,直接丢弃任务,不做处理且不抛出异常,一般用于处理无关紧要的任务。
DiscardOldestPolicy
,丢弃队列中最前面的任务,也就是最老的任务,然后尝试执行新任务。
CallerRunsPolicy
,由调用者线程进行处理。
AbortPolicy
,抛出异常。
笔者的线程池选用的就是这种模式,由专门的线程不断将客户端发来的数据包放入任务队列,每放入一个任务(数据包),就会通过条件变量唤醒一个阻塞的线程,从队列中取出数据包,处理任务。
线程池
,笔者习惯将其理解为一种,将线程池化的技术,众所周知,多线程是高并发的重要实现方式之一,可以极大提高执行任务的效率,但是同时多线程的引入也会带来一些潜在的问题,比如如何安全的创建、管理、和调度线程执行任务?因此,线程池的设想便应运而生。
线程池,是一种组织多线程的形式,通过在初始化阶段一次性创建足够数量的线程,并将这些线程统一保存在指定容器当中,等待有任务到来时,进行调度。简单来讲,可以理解为,有一个很大的池子,里面“养”着很多线程,当需要执行任务时,便从池子中唤醒一个线程去处理。这样的好处是可以避免线程不断创建、销毁带来的开销和不确定性,线程得以复用。
在 Ngina - Server 服务器中使用到了线程池的设计,采用多线程方式处理多条连接到达的数据包,提高服务器效率。
首先创建指定数量的线程(核心线程),线程被创建后进入阻塞,等待任务到达后被唤醒,处理结束后则继续被阻塞。
DiscardPolicy
,直接丢弃任务,不做处理且不抛出异常,一般用于处理无关紧要的任务。
DiscardOldestPolicy
,丢弃队列中最前面的任务,也就是最老的任务,然后尝试执行新任务。
CallerRunsPolicy
,由调用者线程进行处理。
AbortPolicy
,抛出异常。
一般来讲,线程数的设置需要考虑多方面的因素,综合决定。常见的有两种方式:
IO密集型任务
:这类型任务输入输出会更多一些,不是一直在执行任务,因此线程数可以设置的略大一些,参考公式为 2 * Ncpu。
CPU密集型任务
:这类任务会使得 CPU 被频繁占用,因此对于这类任务,线程数应该尽量少,参考的公式为:Ncpu + 1。
但是实际上,一般是结合压测来进行设置,先预先设置一个比较大的线程数,然后进行压测,通过监控CPU和内存的变化来修改线程数。
本章主要讨论线程池相关设计原理,希望能帮助读者更加深入的理解线程池的设计思想。
最后,我是Alkaid#3529,一个追求不断进步的学生,期待你的关注!