深入了解linux系统—— 线程池

线程池

通过之前的学习,了解了Linux线程相关知识,我们可以创建新线程,让新线程去调用函数;

这样我们就可以给新线程分配任务,让新线程去执行,主线程等待

这里如果有任务,再去创建线程,让线程执行,这就会导致多次调用系统调用创建线程,进而影响缓冲局部性和整体性能。

线程池,是一种线程使用模式,通过预先创建线程来减少系统调用的次数。

线程池,和之前实现的进程池一样,预先创建并维护多个线程,这些线程等待分配任务(阻塞等待);当有任务时,唤醒其中的一些线程去执行这些任务;

这样不仅能保证内核的充分利用,还能防止过分调度

线程池使用场景:

  • 需要大量线程来完成任务,且完成任务的时间比较短;例如WEB负服务器完成网页请求。
  • 对性能要求苛刻的应用;例如要求服务器迅速响应客户请求。
  • 接受突发性的大量请求,但不至于服务器因此产生大量线程的应用。

对于线程池,可以简单分为两种:

  1. 固定数量线程池:创建固定数量的线程,循环从人物队列中获取任务对象,获取到任务对象之后执行任务对象中的接口。
  2. 浮动线程池,线程的数量可以根据任务对象数量动态调整。

设计与实现

简单了解了线程池,现在来设计一个创建固定线程个数的线程池。(这里面默认设置线程数量为5

首先,线程池肯定要有线程吧,这里就复用之前是封装好的Thread,使用vector将这些线程存储起来。

其次,也要有任务,这里就使用queue来存储这些任务;

然后,为了保证任务队列中没有任务时,线程要阻塞等待;任务队列中有任务时,要有线程去处理任务;就要存在互斥信号量mutex以及条件变量cond (这里使用之前封装好的MutexCond

最后,在线程池中,我们也可以使用记录一下线程的数量,以及线程的运行状态。

cpp 复制代码
    template <typename T>
    class Threadpool
    {
    private:
        std::vector<Thread> _threadpool; // 线程
        std::queue<T> _taskqueue;        // 任务队列
        Mutex _mutex;                    // 互斥量
        Cond _cond;                      // 条件变量
        int _num;                        // 线程数量
        bool _runing;                    // 运行状态
    };

1. 构造与析构函数

对于一个线程池,创建线程池对象时,要初始化其中的每一个线程;

要初始换线程,就要存在一个func_t类型的函数(using func_t = std::function<void()>;),这里我们可以使用lambda表达式,其中去调用Threadpool的类内方法HandlerTask

对于析构函数,我们要回收线程池中所有的线程;

这里就可以实现一个Join方法,在使用时就可以调用Join回收线程池中的线程;而在析构函数中也要调用Join方法。

cpp 复制代码
        Threadpool(int num = default_num) : _num(num), _runing(false)
        {
            for (int i = 0; i < num; i++)
            {
                _threadpool.emplace_back(
                    [this]()
                    {
                        HandlerTask();
                    })
            }
        }
        void HandlerTask()
        {
            // 线程执行函数...
        }
        void Join()
        {
            for(auto& thread : _threadpool)
            {
                thread.Join();
            }
        }
        ~Threadpool()
        {
            Join();
        }

2. 运行线程池

这里我们复用Thread的代码,在创建出线程池对象时,其实并没有创建线程,线程池也并没有运行起来;

这里就设计实现一个Start方法,来启动线程池(创建线程),让线程池运行起来;并且实现HandlerTask方法(存在BUG

启动线程池

实现Start方法很简单,只需要一次调用每一个线程的Start方法,创建线程即可。

实现HandlerTask

HandlerTask方法,线程池所创建的每一个线程都要执行方法;

所以该方法就要实现线程从线程池中取任务,并且执行该任务。

  • 如果线程池任务队列中存在任务,线程取出并执行该任务。
  • 如果线程池任务队列没有任务,线程就要阻塞等待。

注意:任务队列对于多线程来说,就是临界资源;为了保证数据一致,线程在访问时就要现申请临界资源(申请失败也要阻塞等待)

cpp 复制代码
        void HandlerTask()
        {
            // 线程执行函数...
            while (true)
            {
                LockGroup lgp(_mutex);
                {
                    while (_taskqueue.empty())
                    {
                        _cond.Wait(_mutex.GetMutex());
                    }
                    // 取任务
                    T task = _taskqueue.front();
                    _taskqueue.pop();
                }
                // 处理任务
                sleep(1);
            }
        }
        void Start()
        {
            _runing = true;
            for(auto& thread : _threadpool)
            {
                thread.Start(); 
            }
        }

到这里,实现的线程池已经可以运行起来了;这里测试由于没有像任务队列中存放任务,所有的线程都要阻塞等待。

cpp 复制代码
int main()
{
    Threadpool<int> tpl;
    tpl.Start();
    sleep(10);
    return 0;
}

3. 终止线程池

上述代码,线程池已经可以运行起来了,但是,存在一个BUG

经过测试也不难发现,在程序sleep10秒后,程序要退出,调用Threadpool析构函数,主线程等待并回收新线程;

但是,此时所有的新线程都处于阻塞等待状态,主线程要阻塞等待新线程退出;所以就导致所有的线程都在阻塞等待。

所以,这里我们就要实现一个方法,来终止线程池

首先,终止线程池,将线程池的运行状态设置成false

但是,将线程设置成false之后,其他线程有的可能在执行任务,有的可能在阻塞等待;难道线程池都结束了,这些等待中的线程还用阻塞等待着吗?

所以,将线程池状态设置成false之后,要唤醒所有正在等待的线程

唤醒所有等待的线程之后,问题又来了:

  • 如果此时任务队列中没有任务,这些线程就会再次进入循环,阻塞等待。
  • 如果此时任务队列中存在任务,这些线程是直接退出,还是继续执行任务?

对于第一个问题,这里就需要修改上面的HandlerTask函数,至于如何修改,先看第二个问题;

如果线程池终止了,此时线程池还存在任务,这里就要线程继续执行任务,直到将任务队列中的任务执行完。

所以,线程要阻塞等待的条件不再是 任务队列为空(_taskqueue.empty());而是任务队列为空,并且当前线程池正在运行(_taskqueue.empty() && _runing)

而线程阻塞等待的条件变了,那线程在取任务之前,任务队列就可能为空(线程池运行状态为false,任务队列为空);

所以,在先去从任务队列中取任务之前,要先判断任务队列是否为空;如果为空,当前线程池就结束了,直接break即可;(break后,函数结束,线程也就结束了)如果不为空,才取任务执行任务。

所以,要实现的StopWeakUp和修改后的HandlerTask

cpp 复制代码
    static int default_num = 5;
    template <typename T>
    class Threadpool
    {
        void Weakup()
        {
            LOG(Level::DEBUG) << "唤醒所有线程";
            _cond.Broadcast();
        }
    public:
        void HandlerTask()
        {
            while (true)
            {
                LockGroup lgp(_mutex);
                {
                    while (_taskqueue.empty() && _runing)
                    {
                        _cond.Wait(_mutex.GetMutex());
                    }
                    // 取任务
                    if (_taskqueue.empty())
                    {
                        LOG(Level::DEBUG) << "线程退出";
                        break;
                    }
                    T task = _taskqueue.front();
                    _taskqueue.pop();
                }
                // 处理任务
                sleep(1);
            }
        }
        void Stop()
        {
            _runing = false;
            Weakup();
        }

    private:
        std::vector<Thread> _threadpool; // 线程
        std::queue<T> _taskqueue;        // 任务队列
        Mutex _mutex;                    // 互斥量
        Cond _cond;                      // 条件变量
        int _num;                        // 线程数量
        bool _runing;                    // 运行状态
    };

这里实现Weakup方法,不希望被调用,只在内部使用,就设置成私有。

单例模式

什么是单例模式呢?

就好比现实生活中,一个人只能存在一个合法夫妻一样;

在上述实现的线程池代码中,可以发现线程池整体还是很大的;如果存在多个线程池对象,是十分消耗内存资源的。

在很多服务器开发场景中,经常要让服务器加载很多数据,这是就需要一个单例的来管理这些数据。

简单来说,单例模式就是在内存中只能存在一个对象。

1. 饿汉模式

对于饿汉实现,抽象来说就是:

吃完饭,立即洗碗,在每次吃饭时都可以直接吃。

我们就可以理解为,先创建对象,这样在使用时就可以直接调用。

cpp 复制代码
template <typename T>
class Singleton
{
    static T data;

public:
    static T *GetInstance()
    {
        return &data;
    }
};

这样每次在使用时,通过类直接调用静态成员函数。

此外,单例模式只允许存在一个对象,所以拷贝构造,拷贝赋值等都要私有化private或者删除delete

2. 懒汉模式

对于懒汉模式,抽象来说:吃完饭,先不洗碗,在下次吃饭前再洗。

我们就可以理解为,先不创建对象,在要使用时再创建对象。

这里懒汉实现方式核心思想就是 :延时加载,在使用时在创建对象,优化服务器是启动速度

cpp 复制代码
template <typename T>
class Singleton
{
    static T *inst;

public:
    static T *GetInstance()
    {
        if (inst == NULL)
        {
            inst = new T();
        }
        return inst;
    }
};

3. 实现懒汉单例模式

对于懒汉模式,存在一个问题:线程不安全

在第一次调用GetInstance时,多线程同时访问GetInstance时,就会创建出多个对象

这里就要对其进行加锁

cpp 复制代码
template <typename T>
class Singleton
{
    volatile static T *inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
    static std::mutex lock;

public:
    static T *GetInstance()
    {
        if (inst == NULL)
        {                // 双重判定空指针, 降低锁冲突的概率, 提⾼性能.
            lock.lock(); // 使⽤互斥锁, 保证多线程情况下也只调⽤⼀次 new.
            if (inst == NULL)
            {
                inst = new T();
            }
            lock.unlock();
        }
        return inst;
    }
};

这样实现的单例模式,使用起来也太不方便了,这里将其设计到线程池内部方便使用;

cpp 复制代码
class Threadpool // 注:类名这里应该是 ThreadPool<T>(代码笔误,否则模板无法生效)
{
public:
static ThreadPool<T> *GetInstance()
{
    // 第一次检查:未加锁,快速判断实例是否已存在(避免每次调用都加锁,提高效率)
    if (inc == nullptr)
    {
        // 加锁:确保多线程下只有一个线程能进入"创建实例"的逻辑
        LockGuard lockguard(_lock);
        LOG(LogLevel::DEBUG) << "获取单例....";
        
        // 第二次检查:加锁后再次判断(防止多线程同时通过第一次检查,重复创建实例)
        if (inc == nullptr)
        {
            LOG(LogLevel::DEBUG) << "首次使用单例, 创建之....";
            inc = new ThreadPool<T>();  // 创建线程池实例
            inc->Start();  // 启动线程池(初始化线程、开始接收任务等,具体逻辑在 Start() 中)
        }
    }
    return inc;  // 返回唯一实例指针
}

private:
    // 静态成员:单例实例指针 + 互斥锁(保证线程安全)
    static ThreadPool<T> *inc;
    static Mutex _lock; 
};

// 静态成员的类外初始化(模板类必须在类外显式初始化静态成员)
template <typename T>
ThreadPool<T> *ThreadPool<T>::inc = nullptr;

线程安全和重入

  1. 线程安全:多线程同时访问代码 / 共享资源时,结果与单线程执行一致,无数据损坏或逻辑混乱。核心解决 "共享资源竞争",常用互斥锁、原子操作、无共享设计实现。
  2. 重入问题:函数被中断后重新进入(如中断 / 嵌套调用)仍能正确执行。关键看是否依赖静态 / 全局变量(依赖则不可重入,仅用局部变量通常可重入)。
  3. 关系:可重入≠线程安全(可重入函数多线程调用仍可能因共享资源不安全),线程安全≠可重入(线程安全函数若加锁,重入可能死锁);理想状态是 "可重入且线程安全"(如纯函数)。
相关推荐
AOwhisky4 分钟前
Python 学习笔记(第十四期)——运维自动化(下·中篇):远程文件传输——paramiko进阶篇
运维·python·学习·云原生·自动化·文件传输·paramiko
imc.1133 分钟前
linux基础IO
linux·运维·服务器
躺不平的理查德34 分钟前
让Ubuntu 的第二块网卡 ens37 永久固定为 192.168.30.111
运维·服务器
bitbrowser1 小时前
Facebook 验证反复回到原页面,应该从哪里排查
运维·服务器·facebook
Land03291 小时前
AI网页元素变化无法自动修复?自带元素自愈的自动化解决方案
运维·人工智能·ai·自动化·rpa
fengyehongWorld1 小时前
Jenkins 安装与简单配置
运维·jenkins
云智慧AIOps社区2 小时前
2026 国产化 ITSM 替代指南:横向测评 ServiceNow、轻帆云、Jira等五款主流IT服务管理平台
运维·人工智能·运维开发·it服务管理·itsm平台
糖果店的幽灵2 小时前
langgraph的分支结构之 - 静态分支详解
java·服务器·数据库·langgraph
虹科网络安全2 小时前
艾体宝新闻|从 SQL 注入到服务器接管:CVE-2026-57517 暴露 Web 管理面板的供应链与安全编码风险
服务器·前端·sql
Urbano2 小时前
针织卫衣全流程生产工序科普:自动化替代、产能优化与设备选型实战方案
运维·自动化