基于 Linux 通信架构的 Thread Pool A 线程池分析

文章目录

  • 本章内容概述
  • 一、基本介绍
    • 1. 基本定义
    • 2. 线程池优势
  • 二、线程池属性
    • 1. 线程数量
    • 2. 任务队列
    • 3. 线程数配置
  • 三、线程池设计模式
    • 1. 策略模式
    • 2. 生产者 - 消费者模式
  • 四、线程池常见问题
    • 1. 对线程池的了解?
    • 2. 你用过线程池吗?用到哪些场景?
    • 3. 线程池的运行原理你知道吗?
    • 4. 线程池的拒绝策略了解过吗?
    • 5. 在实际使用线程池的过程中线程数时怎么设置的?
  • 本章总结


本章内容概述

本文用于笔者对在开发 Ngina - server 服务器框架过程中,曾设计过的线程池任务调度进行相关分析,主要包括线程池设计思路、线程池实现原理和应用进行详细阐述,希望可以帮助读者更好的理解线程池这一设计理念。


一、基本介绍

1. 基本定义

线程池,笔者习惯将其理解为一种,将线程池化的技术,众所周知,多线程是高并发的重要实现方式之一,可以极大提高执行任务的效率,但是同时多线程的引入也会带来一些潜在的问题,比如如何安全的创建、管理、和调度线程执行任务?因此,线程池的设想便应运而生。

线程池,是一种组织多线程的形式,通过在初始化阶段一次性创建足够数量的线程,并将这些线程统一保存在指定容器当中,等待有任务到来时,进行调度。简单来讲,可以理解为,有一个很大的池子,里面“养”着很多线程,当需要执行任务时,便从池子中唤醒一个线程去处理。这样的好处是可以避免线程不断创建、销毁带来的开销和不确定性,线程得以复用。

除了线程池之外,还有内存池、连接池等其他池化技术,应用范围十分广泛。

2. 线程池优势

相比于传统的,不断创建、销毁线程的方式,池化技术的引入带来了很多便捷之处。

控制资源,预先设定线程数量上限,可以避免 QPS 过高导致服务器资源耗尽、线程创建失败产生的一系列问题。

线程复用,反复的创建和销毁都需要一定的时空开销,因此线程池可以在一定程度上提升程序性能。

当然,线程池也有他的缺点,如线程池的数量配置需要合理,线程间通信、共享数据保护等。

二、线程池属性

1. 线程数量

核心线程数,是线程池中常住的线程数量,一般来讲线程池中的线程数不应当少于此数值,用于在大多数情况下处理任务,在任务不多的情况下会被阻塞。

最大线程数,除了核心线程之外,会在必要情况下创建一定数量的线程解决短时间内到达的大量任务,这部分线程被创建出后一般会立刻投入使用,处理任务结束后会被回收,等待销毁。

2. 任务队列

任务队列,即存放待处理任务的队列,队列组织方式大致有以下几种:

  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • DelayQueue
  • SynchronousQueue
  • LinkedBlockingDeque

ArrayBlockingQueue ,是由数组实现的有界的阻塞队列,在初始化的时候,必须指定大小。

LinkedBlockingQueue ,是由链表实现的无界的阻塞队列,可以在初始化时指定大小。

DelayQueue,延迟队列,只有延迟期满足才会从队列中获取元素。

SynchronousQueue,是一个不存储元素的阻塞队列:若是插入时,已经有一个元素,就会阻塞等待,直到这个元素被移除,反之亦然。

LinkedBlockingDeque,是一个由链表组成的双向阻塞队列。

3. 线程数配置

一般来讲,线程数的设置需要考虑多方面的因素,综合决定。常见的有两种方式:

  • IO密集型
  • CPU密集型

IO密集型任务:这类型任务输入输出会更多一些,不是一直在执行任务,因此线程数可以设置的略大一些,参考公式为 2 * Ncpu。

CPU密集型任务:这类任务会使得 CPU 被频繁占用,因此对于这类任务,线程数应该尽量少,参考的公式为:Ncpu + 1。

但是实际上,一般是结合压测来进行设置,先预先设置一个比较大的线程数,然后进行压测,通过监控CPU和内存的变化来修改线程数。

三、线程池设计模式

1. 策略模式

线程池中线程数存在上限,因此,任务队列中不可避免的会堆积任务,当任务数量超出队列上限后如何处理?这就涉及到线程池的拒绝策略了,大致有以下几种:

  • DiscardPolicy
  • DiscardOldestPolicy
  • CallerRunsPolicy
  • AbortPolicy

DiscardPolicy,直接丢弃任务,不做处理且不抛出异常,一般用于处理无关紧要的任务。

DiscardOldestPolicy,丢弃队列中最前面的任务,也就是最老的任务,然后尝试执行新任务。

CallerRunsPolicy,由调用者线程进行处理。

AbortPolicy,抛出异常。

2. 生产者 - 消费者模式

笔者的线程池选用的就是这种模式,由专门的线程不断将客户端发来的数据包放入任务队列,每放入一个任务(数据包),就会通过条件变量唤醒一个阻塞的线程,从队列中取出数据包,处理任务。

四、线程池常见问题

1. 对线程池的了解?

线程池,笔者习惯将其理解为一种,将线程池化的技术,众所周知,多线程是高并发的重要实现方式之一,可以极大提高执行任务的效率,但是同时多线程的引入也会带来一些潜在的问题,比如如何安全的创建、管理、和调度线程执行任务?因此,线程池的设想便应运而生。

线程池,是一种组织多线程的形式,通过在初始化阶段一次性创建足够数量的线程,并将这些线程统一保存在指定容器当中,等待有任务到来时,进行调度。简单来讲,可以理解为,有一个很大的池子,里面“养”着很多线程,当需要执行任务时,便从池子中唤醒一个线程去处理。这样的好处是可以避免线程不断创建、销毁带来的开销和不确定性,线程得以复用。

2. 你用过线程池吗?用到哪些场景?

在 Ngina - Server 服务器中使用到了线程池的设计,采用多线程方式处理多条连接到达的数据包,提高服务器效率。

3. 线程池的运行原理你知道吗?

首先创建指定数量的线程(核心线程),线程被创建后进入阻塞,等待任务到达后被唤醒,处理结束后则继续被阻塞。

4. 线程池的拒绝策略了解过吗?

DiscardPolicy,直接丢弃任务,不做处理且不抛出异常,一般用于处理无关紧要的任务。

DiscardOldestPolicy,丢弃队列中最前面的任务,也就是最老的任务,然后尝试执行新任务。

CallerRunsPolicy,由调用者线程进行处理。

AbortPolicy,抛出异常。

5. 在实际使用线程池的过程中线程数时怎么设置的?

一般来讲,线程数的设置需要考虑多方面的因素,综合决定。常见的有两种方式:

  • IO密集型
  • CPU密集型

IO密集型任务:这类型任务输入输出会更多一些,不是一直在执行任务,因此线程数可以设置的略大一些,参考公式为 2 * Ncpu。

CPU密集型任务:这类任务会使得 CPU 被频繁占用,因此对于这类任务,线程数应该尽量少,参考的公式为:Ncpu + 1。

但是实际上,一般是结合压测来进行设置,先预先设置一个比较大的线程数,然后进行压测,通过监控CPU和内存的变化来修改线程数。


本章总结

本章主要讨论线程池相关设计原理,希望能帮助读者更加深入的理解线程池的设计思想。

最后,我是Alkaid#3529,一个追求不断进步的学生,期待你的关注!

你可能感兴趣的:(C++,面试核心与项目设计,架构,服务器,java)