深入了解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. 关系:可重入≠线程安全(可重入函数多线程调用仍可能因共享资源不安全),线程安全≠可重入(线程安全函数若加锁,重入可能死锁);理想状态是 "可重入且线程安全"(如纯函数)。
相关推荐
不是编程家2 小时前
Linux第十五讲:Socket编程UDP
linux·运维·udp
UrSpecial2 小时前
Linux线程
linux·开发语言·c++
格林威3 小时前
Linux使用-MySQL的使用
linux·运维·人工智能·数码相机·mysql·计算机视觉·视觉检测
程序员TNT3 小时前
Shoptnt 促销计算引擎详解:策略模式与责任链的完美融合
linux·windows·策略模式
大锦终3 小时前
【Linux】进程间通信
linux·运维·服务器·c++
望获linux3 小时前
【实时Linux实战系列】规避缺页中断:mlock/hugetlb 与页面预热
java·linux·服务器·数据库·chrome·算法
澡点睡觉3 小时前
【前沿技术拓展Trip one】 芯片自动化和具身智能
运维·自动化
成都极云科技3 小时前
独立显卡和集成显卡切换电脑卡住了怎么办?
linux·电脑·集成显卡·独立显卡
To_再飞行3 小时前
K8s访问控制(二)
linux·网络·云原生·容器·kubernetes