【Linux笔记】——线程池项目与线程安全单例模式

🔥个人主页🔥:孤寂大仙V

🌈收录专栏🌈:Linux

🌹往期回顾🌹: 【Linux笔记】------简单实习一个日志项目

🔖流水不争,争的是滔滔不息


一、线程池设计

线程池

一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度,可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。线程池的主要优点是减少在创建和销毁线程上所花的时间以及系统资源的开销。通过重用已存在的线程,线程池可以显著提高系统性能,特别是在需要处理大量短生命周期任务的场景中。

使用场景

需要大量的线程来完成任务,且完成任务的时间比较短。比如WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。

对性能要求苛刻的应用,比如要求服务器迅速响应客户的请求。

接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

二、线程池代码

ThreadPool.hpp线程池主体逻辑

cpp 复制代码
#pragma once

#include <iostream>
#include <vector>
#include <queue>
#include <functional>
#include "Log.hpp"
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
using namespace std;
using namespace MutexModule;
using namespace CondModule;
using namespace ThreadModule;
using namespace LogModule;

const int gnum=5;
namespace ThreadPoolModule
{
    template<class T>
    class ThreadPool
    {
    public:
        ThreadPool(int num=gnum)
        :_num(num)
        ,_isrunning(false)
        ,_sleepnum(0)
        {
            for(int i=0;i<_num;i++)
            {
                _thread.emplace_back([this](){
                    HandlerTask();
                });
            }
        }

        void Threadone()
        {
            _cond.Signal();
            LOG(LogLevel::INFO) << "唤醒一个休眠线程";
        }

        void Threadall()
        {
            LockGuard lockguard (_mutex);
            if(_sleepnum>0)
            {
                _cond.Broadcast();
            }
            LOG(LogLevel :: INFO)<<"唤醒所有休眠线程";
        }

        void Start()
        {
            if(_isrunning) return;
            _isrunning=true;
            for(auto &thread :_thread)
            {
                thread.Start();
            }
            LOG(LogLevel :: INFO)<<"开始创建线程池";
        }
  
        void Stop()
        {
            if(!_isrunning) return;
            _isrunning =false;
            Threadall();//让等待的进程全部启动
        }

        void Join()
        {
            for(auto &thread :_thread)
            {
                thread.Join();
            }
            LOG(LogLevel :: INFO)<<"线程回收";
        }
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while(true)
            {
                T t;
                {
                    LockGuard lockguard (_mutex);

                    if(_taskq.empty() && _isrunning) //把全部休眠的线程启动,必须保证进程池已经退出状态,要不就陷入死循环
                    {
                        _sleepnum++;//如果等待计数++
                        _cond.Wait(_mutex);
                        _sleepnum--;//退出等待计数--
                    }

                    if(_taskq.empty() && !_isrunning) //等待后唤醒,必须是任务队列为空 进程池退出
                    {
                        LOG(LogLevel :: INFO)<<name<<"退出了,任务队列为空,进程池退出";
                        break;
                    }

                    t=_taskq.front();
                    _taskq.pop();
                }
                t();//执行任务
            }
            
        }

        bool Enqueue (const T& in)
        {
            if(_isrunning)
            {
                LockGuard LockGuard (_mutex);
                _taskq.push(in);
                if(_sleepnum==_thread.size())
                {
                    Threadone();
                }
                return true;
            }
            return false;    

        }

        ~ThreadPool()
        {

        }

    private:
        vector<Thread> _thread;
        queue<T> _taskq;
        int _num;
        Mutex _mutex;
        Cond _cond;
        bool _isrunning;
        int _sleepnum;

    };
}

这里用到了之前封装好的线程、条件变量互斥与同步、日志。

私有成员变量

_thread我们用vector数组充当线程池,_taskq任务队列用的是queue队列,_num是线程池中的线程的个数(我们写的是固定线程的线程池),_isrunning判断线程是否运行,_sleepnum是线程等待的个数。


cpp 复制代码
		ThreadPool(int num=gnum)
        :_num(num)
        ,_isrunning(false)
        ,_sleepnum(0)
        {
            for(int i=0;i<_num;i++)
            {
                _thread.emplace_back([this](){
                    HandlerTask();
                });
            }
        }
        
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while(true)
            {
                T t;
                {
                    LockGuard lockguard (_mutex);

                    if(_taskq.empty() && _isrunning) //把全部休眠的线程启动,必须保证进程池已经退出状态,要不就陷入死循环
                    {
                        _sleepnum++;//如果等待计数++
                        _cond.Wait(_mutex);
                        _sleepnum--;//退出等待计数--
                    }

                    if(_taskq.empty() && !_isrunning) //等待后唤醒,必须是任务队列为空 进程池退出
                    {
                        LOG(LogLevel :: INFO)<<name<<"退出了,任务队列为空,进程池退出";
                        break;
                    }

                    t=_taskq.front();
                    _taskq.pop();
                }
                t();//执行任务
            }

上面的代码是构造函数构造线程池,这里主要阐述,构造函数创建线程池与线程去执行任务的函数的关系。

构造函数通过一个for循环,我们创建了_num个Thread对象,每个对象都绑定一个lambda,lambda里面调用的是线程池的成员函数HandlerTask()(在类内调用类内成员函数用lambda),这些lambda是"线程的入口函数",它们一启动就跑进HandlerTask()中并一直在那里循环干活。


cpp 复制代码
void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while(true)
            {
                T t;
                {
                    LockGuard lockguard (_mutex);

                    if(_taskq.empty() && _isrunning) //把全部休眠的线程启动,必须保证进程池已经退出状态,要不就陷入死循环
                    {
                        _sleepnum++;//如果等待计数++
                        _cond.Wait(_mutex);
                        _sleepnum--;//退出等待计数--
                    }

                    if(_taskq.empty() && !_isrunning) //等待后唤醒,必须是任务队列为空 进程池退出
                    {
                        LOG(LogLevel :: INFO)<<name<<"退出了,任务队列为空,进程池退出";
                        break;
                    }

                    t=_taskq.front();
                    _taskq.pop();
                }
                t();//执行任务
            }

上面这段代码。一启动,线程池的每个线程都会进去,所以加锁。如果任务队列是空的并且这个线程池已经启动了,那么线程就要进入等待队列(条件变量同步),注意要写一个计数的变量需要进入等待队列就要++出来就--(这里+±-就是为了唤醒休眠线程的时候进行判断,颗粒度更细)。如果任务队列为空并且线程池也不在运行了,直接break退出回收线程。最后满足线程能拿到任务,取出队首的任务,执行任务。


cpp 复制代码
		//唤醒单个线程
        void Threadone()
        {
            _cond.Signal();
            LOG(LogLevel::INFO) << "唤醒一个休眠线程";
        }
		//唤醒所有进程
        void Threadall()
        {
            LockGuard lockguard (_mutex);
            if(_sleepnum>0)
            {
                _cond.Broadcast();
            }
            LOG(LogLevel :: INFO)<<"唤醒所有休眠线程";
        }
		//创建线程池
        void Start()
        {
            if(_isrunning) return;
            _isrunning=true;
            for(auto &thread :_thread)
            {
                thread.Start();
            }
            LOG(LogLevel :: INFO)<<"开始创建线程池";
        }
  		//终止线程
        void Stop()
        {
            if(!_isrunning) return;
            _isrunning =false;
            Threadall();//让等待的进程全部启动
        }
		//回收线程
        void Join()
        {
            for(auto &thread :_thread)
            {
                thread.Join();
            }
            LOG(LogLevel :: INFO)<<"线程回收";
        }
        //任务队列中放任务
		bool Enqueue (const T& in)
        {
            if(_isrunning)
            {
                LockGuard LockGuard (_mutex);
                _taskq.push(in);
                if(_sleepnum==_thread.size())
                {
                    Threadone();
                }
                return true;
            }
            return false;    

        }

唤醒单个线程直接调用之前封装好的条件变量同步,唤醒所有休眠的线程,如果之前计数的_sleepnum>0就要唤醒所有的休眠线程了。

启动线程就是创建vector数组里的线程,创建线程池。

进程终止,调用之前封装的条件变量。

回收线程,调用之前封装的条件变量。

Enqueue就是往任务队列里放任务,if(_sleepnum==_thread.size()) 判断是否所有线程都在休眠,避免唤醒线程池中已经在忙的线程(节省上下文切换开销)如果不判断 _sleepnum,直接 Threadone() 会怎样?可能会导致重复唤醒甚至无意义的上下文切换。比如:有5个线程,其中2个在处理任务,3个在睡觉;你来了个新任务,就唤醒1个线程;但其实可能原本某个线程马上就处理完会抢新任务;你提前唤醒一个,就多了一次线程上下文切换(白唤醒了)。


main.cc

cpp 复制代码
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"

using namespace LogModule;
using namespace ThreadPoolModule;

int main()
{
    Enable_Console_Log_Strategy();

    int cnt=5;
    ThreadPool<task_t>* tp=new ThreadPool <task_t> ();
    tp->Start();
    
    while(cnt)
    {
        sleep(1);
        tp->Enqueue(Download);
        cnt--;
    }
   
    tp->Stop();
    tp->Join();
    return 0;
}

Task.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <functional>
#include "Log.hpp"

using namespace LogModule;

using task_t = std::function<void()>;

void Download()
{
   LOG(LogLevel::DEBUG) << "我是一个下载任务...";
}

这里的function就简单写了一下,没有放具体的任务,但是要走到这里用的是function的语法


线程池就是通过一个vector数组(这里这么写的也可以是别的)里面创建线程,其实就是把线程准备好放在vector数组中,任务来了线程直接就能用,大大提高了效率。线程池就是提前创建一批线程放在池子里反复复用,避免任务来了才临时创建/销毁线程造成的高开销。

常规线程池:源码

三、线程安全的单例模式

单例模式

单例模式是一种设计模式,确保一个类只有一个实例(对象),并提供一个全局访问点来获取该实例。这种模式通常用于控制资源的访问,例如数据库连接、日志记录器等,以避免创建多个实例导致资源浪费或冲突。

饿汉模式实现单例

一开始就创建好单例对象。程序一启动,就创建好对象了,饿的不行

懒汉模式实现单例

什么时候用什么时候创建单例对象。防止创建多个对象。第一次用对象是才创建,是不是很懒。


线程安全的懒汉式单例模式

禁用拷贝构造和赋值重载

cpp 复制代码
ThreadPool(const ThreadPool<T> &) = delete;               // 把拷贝构造给禁用 没办法直接创建对象
ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // 把赋值重载禁用

显示的禁掉拷贝构造和赋值重载,单例模式的最大的特征以及核心就是,保证全程只有一个对象。如果不禁掉拷贝构造和赋值重载就会出现多个对象,完全违背了单例迷失的初衷。
没有禁用拷贝构造和赋值重载,单例根本不单例。


类外初始化单例指针

cpp 复制代码
template <class T>
class ThreadPool {
public:
    static ThreadPool<T>* inc;
    // 其他成员...
};


template <class T>
ThreadPool<T>* ThreadPool<T>::inc = nullptr;// 类外初始化

template <class T>
Mutex ThreadPool<T>::_lock; // 类外初始化

类内声明的静态成员变量,必须类外初始化。 static ThreadPool* inc这个静态成员变量必须类外初始化。原因是:静态成员变量属于类本身,而不是某个对象。类声明只是告诉编译器"这里有这么个静态变量",但不分配内存。只有在类外定义后,编译器才会给它分配内存空间。

为什么要加static成为静态成员变量?


创建单例对象

cpp 复制代码
static ThreadPool<T> *GetInstance()
        {
            if (inc == nullptr) // 多加一层多一层保护  
            {
                LockGuard lockguard(_lock); // 为防止多线程访问,加锁
                if (inc == nullptr) //第一次用这个inc单例指针对象
                {
                    LOG(LogLevel::DEBUG) << "首次使用单例, 创建之....";
                    inc = new ThreadPool<T>();  //创建单例指针对象
                }
            }

            return inc;
        }

这里主要就是创建单例指针对象,如果这里不加static如果想调用这个函数是不是就要创建这个类的对象就违背了单例的初衷。所以这里要让它成为静态成员函数。这也回答了上面为什么要用static成员变量,在这个静态成员函数中创建单例指针对象,要使用静态成员变量。

内层中的if (inc == nullptr)是判断是否是第一次用这个inc单例指针对象,如果没有创建单例指针对象。加锁是为了防止多线程访问。在外层的if (inc == nullptr),首先不加外层的这个if (inc == nullptr),线程来了访问互斥锁,拿到锁的线程进去创建单例指针对象,那么多线程是不是每次不管是否已经创建了单例指针对象都要拿锁然后进去转一圈在返回这个单例指针对象,是不是效率就会降低。加上最外侧的if (inc == nullptr) 每次线程来了,如果已经有单例指针对象了就不要进去在转一圈,直接返回对象就完了,大大提高了效率。


Main.cc

cpp 复制代码
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"

using namespace LogModule;
using namespace ThreadPoolModule;

int main()
{
    Enable_Console_Log_Strategy();//

    int cnt=5;
    // ThreadPool<task_t>* tp=new ThreadPool <task_t> ();
    ThreadPool<task_t> ::GetInstance()->Start();
    
    while(cnt)
    {
        sleep(1);
        ThreadPool<task_t> ::GetInstance()->Enqueue(Download);
        cnt--;
    }
   
    ThreadPool<task_t> ::GetInstance()->Stop();
    ThreadPool<task_t> ::GetInstance()->Join();
    return 0;
}

单例模式如何访问类内成员函数呢?不用创建类的对象,直接用作用域解释符,调用静态成员函数。这样获取进程池的唯一对象,然后通过指针调用方法创建线程池。

懒汉式单例模式线程池

cpp 复制代码
#pragma once

#include <iostream>
#include <vector>
#include <queue>
#include <functional>
#include "Log.hpp"
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
using namespace std;
using namespace MutexModule;
using namespace CondModule;
using namespace ThreadModule;
using namespace LogModule;

const int gnum = 5;
namespace ThreadPoolModule
{
    template <class T>
    class ThreadPool
    {
    private:
        ThreadPool(int num = gnum)
            : _num(num), _isrunning(false), _sleepnum(0)
        {
            for (int i = 0; i < _num; i++)
            {
                _thread.emplace_back([this]()
                                     { HandlerTask(); });
            }
        }

        void Threadone()
        {
            _cond.Signal();
            LOG(LogLevel::INFO) << "唤醒一个休眠线程";
        }

        void Threadall()
        {
            LockGuard lockguard(_mutex);
            if (_sleepnum > 0)
            {
                _cond.Broadcast();
            }
            LOG(LogLevel ::INFO) << "唤醒所有休眠线程";
        }

        ThreadPool(const ThreadPool<T> &) = delete;               // 把拷贝构造给禁用 没办法直接创建对象
        ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // 把赋值重载禁用
    public:
        
        static ThreadPool<T> *GetInstance()
        {
            if (inc == nullptr) // 多加一层多一层保护  
            {
                LockGuard lockguard(_lock); // 为防止多线程访问,加锁
                if (inc == nullptr) //第一次用这个inc单例指针对象
                {
                    LOG(LogLevel::DEBUG) << "首次使用单例, 创建之....";
                    inc = new ThreadPool<T>();  //创建单例指针对象
                }
            }

            return inc;
        }

        void Start()
        {
            if (_isrunning)
                return;
            _isrunning = true;
            for (auto &thread : _thread)
            {
                thread.Start();
            }
            LOG(LogLevel ::INFO) << "开始创建线程池";
        }

        void Stop()
        {
            if (!_isrunning)
                return;
            _isrunning = false;
            Threadall(); // 让等待的进程全部启动
        }

        void Join()
        {
            for (auto &thread : _thread)
            {
                thread.Join();
            }
            LOG(LogLevel ::INFO) << "线程回收";
        }
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while (true)
            {
                T t;
                {
                    LockGuard lockguard(_mutex);

                    if (_taskq.empty() && _isrunning) // 把全部休眠的线程启动,必须保证进程池已经退出状态,要不就陷入死循环
                    {
                        _sleepnum++; // 如果等待计数++
                        _cond.Wait(_mutex);
                        _sleepnum--; // 退出等待计数--
                    }

                    if (_taskq.empty() && !_isrunning) // 等待后唤醒,必须是任务队列为空 进程池退出
                    {
                        LOG(LogLevel ::INFO) << name << "退出了,任务队列为空,进程池退出";
                        break;
                    }

                    t = _taskq.front();
                    _taskq.pop();
                }
                t(); // 执行任务
            }
        }

        bool Enqueue(const T &in)
        {
            if (_isrunning)
            {
                LockGuard LockGuard(_mutex);
                _taskq.push(in);
                if (_sleepnum == _thread.size())
                {
                    Threadone();
                }
                return true;
            }
            return false;
        }

        ~ThreadPool()
        {
        }

    private:
        vector<Thread> _thread;
        queue<T> _taskq;
        int _num;
        Mutex _mutex;
        Cond _cond;
        bool _isrunning;
        int _sleepnum;
        static ThreadPool<T> *inc;
        static Mutex _lock;
    };

    template <class T>
    ThreadPool<T> *ThreadPool<T>::inc = nullptr; // 类外初始化

    template <class T>
    Mutex ThreadPool<T>::_lock; // 类外初始化
}

运行结果

懒汉式单例模式线程池:源码

四、线程安全和重入问题

线程安全:就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。⼀般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进行操作,并且没有锁保护的情况下,容易出现该问题。

重入:同⼀个函数被不同的执行流调用,当前⼀个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。⼀个函数在重⼊的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。学到现在,其实我们已经能理解重入其实可以分为两种情况1.多线程重入函数2.信号导致⼀个执行流重复进入函数


可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的(其实知道这⼀句话就够了)
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果⼀个函数中有全局变量,那么这个函数既不是线程安全也不是可重人的。

可重入与线程安全区别

  • 可重入函数是线程安全函数的⼀种
  • 线程安全不一定是可重入的,而可重入函数则⼀定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

如果不考虑信号导致一个执行流重复进入函数这种重入情况,线程安全和重入在安全角度不做区分。

但是线程安全侧重说明线程访问公共资源的安全情况,表现的是并发线程的特点。

可重入描述的是一个函数是否被重复进入,表示的是函数特点。

相关推荐
磊灬泽24 分钟前
【日常错误】鼠标无反应
linux·windows
知白守黑26727 分钟前
Ansible角色
运维·服务器·ansible
Jwest202127 分钟前
工业显示器在地铁电力监控与运维中的应用
运维·计算机外设
汇能感知3 小时前
摄像头模块在运动相机中的特殊应用
经验分享·笔记·科技
阿巴Jun3 小时前
【数学】线性代数知识点总结
笔记·线性代数·矩阵
茯苓gao4 小时前
STM32G4 速度环开环,电流环闭环 IF模式建模
笔记·stm32·单片机·嵌入式硬件·学习
是誰萆微了承諾4 小时前
【golang学习笔记 gin 】1.2 redis 的使用
笔记·学习·golang
DKPT4 小时前
Java内存区域与内存溢出
java·开发语言·jvm·笔记·学习
Miracle&5 小时前
2.TCP深度解析:握手、挥手、状态机、流量与拥塞控制
linux·网络·tcp/ip
ST.J5 小时前
前端笔记2025
前端·javascript·css·vue.js·笔记