Linux中线程池的简单实现 -- 线程安全的日志模块,策略模式,线程池的封装设计,单例模式,饿汉式单例模式,懒汉式单例模式

目录

[1. 对线程池的理解](#1. 对线程池的理解)

[1.1 基本概念](#1.1 基本概念)

[1.2 工作原理](#1.2 工作原理)

[1.3 线程池的优点](#1.3 线程池的优点)

[2. 日志与策略模式](#2. 日志与策略模式)

[2.1 日志认识](#2.1 日志认识)

[2.2 策略模式](#2.2 策略模式)

[2.2.1 策略模式的概念](#2.2.1 策略模式的概念)

[2.2.2 工作原理](#2.2.2 工作原理)

[2.2 自定义日志系统的实现](#2.2 自定义日志系统的实现)

[3. 线程池设计](#3. 线程池设计)

[3.1 简单线程池的设计](#3.1 简单线程池的设计)

[3.2 线程安全的单例模式线程池的设计](#3.2 线程安全的单例模式线程池的设计)

[3.2.1 单例模式](#3.2.1 单例模式)

[3.2.1.1 概念](#3.2.1.1 概念)

[3.2.1.2 设计原理](#3.2.1.2 设计原理)

[3.2.2 饿汉实现方式](#3.2.2 饿汉实现方式)

[3.2.3 懒汉实现方式](#3.2.3 懒汉实现方式)


1. 对线程池的理解

线程池是一种多线程处理形式,用于管理和复用线程资源,以提高程序的性能和效率。

1.1 基本概念

线程池是一种线程的使用模式,它预先创建一定数量的线程,并将这些线程存储在一个"池"(容器)中,当有新的任务提交时,线程池会从池中取出一个空闲的线程来执行该任务;如果池中没有空闲线程且线程数量未达到最大限制,会创建新的线程来处理任务;若线程数量已达到最大限制,任务会被放入任务队列中等待。任务执行完毕后,线程不会被销毁,而是返回到线程池中,等待下一个任务。

1.2 工作原理

(1)线程池的初始化:在创建线程池时,会根据配置参数创建一定数量的线程,并将它们置于就绪状态,等待任务的到来。

(2)任务提交:当有新的任务需要执行时,将任务添加到任务队列中。

(3)线程分配:线程池中的线程会不断地从任务队列中获取任务。如果队列中有任务,线程会取出任务并执行;如果队列为空,线程会进入等待状态,直到有新的任务加入。

(4)任务执行:线程获取到任务后,会执行任务中的逻辑。任务执行完成后,线程会释放资源,并再次回到线程池中等待下一个任务。

(5)线程池的关闭:当线程池不再需要使用时,可以关闭线程池。关闭过程中,会停止接受新的任务,并等待已有的任务执行完毕,最后销毁所有线程。

1.3 线程池的优点

(1)**提高性能:避免了频繁创建和销毁线程带来的开销。**线程的创建和销毁是比较昂贵的操作,使用线程池可以复用已有的线程,减少了系统资源的消耗,提高了程序的响应速度。

(2)**资源管理:**可以对线程的数量进行有效控制,避免因创建过多线程导致系统资源耗尽。通过设置线程池的最大线程数,可以确保系统在高并发情况下仍能稳定运行。

(3)**提高响应速度:**由于线程池中的线程已经预先创建好,当有任务提交时,可以立即分配线程执行任务,减少了任务的等待时间。

2. 日志与策略模式

2.1 日志认识

计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态,记录异常信息,帮助快速定位问题并支持程序员进行问题修复。是系统维护,故障排除和安全管理的重要工具。

日志有现有的解决方案,如:spdlog,glog,Boost.Log,Log4cxx等等。

下面实现一个自定义类型的日志系统,日志的格式有以下指标:时间,日志等级,进程pid,输出日志的文件,行号,日志内容。

下列是期望输出的日志格式:

2.2 策略模式

2.2.1 策略模式的概念

策略模式定义了一系列算法,将每个算法都封装起来,并且使它们可以互相替换。该模式让算法的变化独立于使用算法的客户,属于行为型模式。通过使用策略模式,可以将算法的选择和实现分离,使得代码更加灵活、可维护和可扩展。

2.2.2 工作原理

(1) 策略接口或抽象类:定义了一个公共的接口或抽象类,用于规范具体策略类的行为。这个接口或抽象类中声明了一个或多个抽象方法,这些方法代表了不同策略可以执行的操作。

(2) 具体策略类:实现了策略接口或继承自抽象策略类,每个具体策略类都实现了特定的算法或行为。它们是策略模式的核心,负责具体的业务逻辑实现。

(3) 上下文类 :持有一个策略接口的引用,通过该引用调用具体策略类的方法来执行相应的算法。上下文类可以根据不同的条件或用户需求,动态地切换使用不同的具体策略类。本质上就是多态特性。

2.2 自定义日志系统的实现

由于实现的日志系统要支持多线程程序日志的有序打印,所以不管在访问显示器还是访问文件的时候都需要通过加锁来维护线程之间的互斥关系。

这里对 Linux 系统中的 pthread 库中的互斥锁进行面向对象的封装:

cpp 复制代码
// Mutex.hpp

#pragma once 
#include <pthread.h>

// 将互斥量接口封装成面向对象的形式
namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            int n = pthread_mutex_init(&_mutex, nullptr);
            (void)n;
        }
        ~Mutex()
        {
            int n = pthread_mutex_destroy(&_mutex);
            (void)n;
        }

        void Lock()
        {
            int n = pthread_mutex_lock(&_mutex);
            (void)n;
        }

        void Unlock()
        {
            int n = pthread_mutex_unlock(&_mutex);
            (void)n;
        }

        pthread_mutex_t* Get()  //  获取原生互斥量的指针
        {
            return &_mutex;
        }
    private:
        pthread_mutex_t _mutex;
    };
    
    // 采用RAII风格进行锁管理,当局部临界区代码运行完的时候,局部LockGuard类型的对象自动进行释放,调用析构函数释放锁
    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex)
        : _mutex(mutex)
        {
            _mutex.Lock();
        }

        ~LockGuard()
        {
            _mutex.Unlock();
        }
    private:
        Mutex& _mutex;
    };
}

实现的一些细节在下列代码中的注释中有所体现。

cpp 复制代码
// Log.hpp

#ifndef __LOG_HPP__
#define __LOG_HPP__

#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <ctime>
#include <memory>
#include <unistd.h>
#include "Mutex.hpp"

namespace LogModule
{
    using namespace MutexModule;

    const std::string gsep = "\r\n";
    // 策略模式 -- 利用C++的多态特性
    // 1. 刷新策略 a: 向显示器打印 b: 向文件中写入
    // 刷新策略基类
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;
    };

    // 显示器打印日志的策略
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy()
        {
        }

        void SyncLog(const std::string &message) override
        {
            // 加锁使多线程原子性的访问显示器
            LockGuard lockGuard(_mutex);
            std::cout << message << gsep;
        }

        ~ConsoleLogStrategy()
        {
        }

    private:
        Mutex _mutex;
    };

    // 文件打印日志策略
    // 默认的日志文件路径和日志文件名
    const std::string defaultPath = "./log";
    const std::string defaultFile = "my.log";
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultPath, const std::string &file = defaultFile)
            : _path(path),
              _file(file)
        {
            // 加锁使多线程原子性的访问文件
            LockGuard lockGuard(_mutex);
            // 判断目录是否存在
            if (std::filesystem::exists(_path)) // 检测文件系统对象(文件,目录,符号链接等)是否存在
            {
                return;
            }
            try
            {
                // 如果目录不存在,递归创建目录
                std::filesystem::create_directories(_path);
            }
            catch (const std::filesystem::filesystem_error &e) // 如果创建失败则打印异常信息
            {
                std::cerr << e.what() << '\n';
            }
        }

        void SyncLog(const std::string &message) override
        {
            LockGuard lockGuard(_mutex);
            // 追加方式向文件中写入
            std::string fileName = _path + (_path.back() == '/' ? "" : "/") + _file;
            // std::ofstream是C++标准库中用于输出到文件的流类,主要用于将数据写入文件
            std::ofstream out(fileName, std::ios::app);
            if (!out.is_open())
            {
                return;
            }
            out << message << gsep;
            out.close();
        }

        ~FileLogStrategy()
        {
        }

    private:
        std::string _path; // 日志文件所在路径
        std::string _file; // 日志文件本身
        Mutex _mutex;
    };

    // 2. 形成完整日志并刷新到指定位置
    // 2.1 日志等级
    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    // 2.2 枚举类型的日志等级转换为字符串类型
    std::string Level2Str(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    // 2.3 获取当前时间的函数
    std::string GetCurTime()
    {
        // time 函数参数为一个time_t类型的指针,若该指针不为NULL,会把获取到的当前时间值存储在指针指向的对象中
        // 若传入为NULL,则仅返回当前时间,返回从1970年1月1日0点到目前的秒数
        time_t cur = time(nullptr);
        struct tm curTm;
        // localtime_r是localtime的可重入版本,主要用于将time_t类型表示的时间转换为本地时间,存储在struct tm 结构体中
        localtime_r(&cur, &curTm);
        char timeBuffer[128];
        snprintf(timeBuffer, sizeof(timeBuffer), "%4d-%02d-%02d %02d:%02d:%02d",
                 curTm.tm_year + 1900,
                 curTm.tm_mon + 1,
                 curTm.tm_mday,
                 curTm.tm_hour,
                 curTm.tm_min,
                 curTm.tm_sec);
        return timeBuffer;
    }

    // 2.4 日志形成并刷新
    class Logger
    {
    public:
        // 默认刷新到显示器上
        Logger()
        {
            EnableConsoleLogStrategy();
        }

        void EnableConsoleLogStrategy()
        {
            // std::make_unique用于创建并返回一个std::unique_ptr对象
            _fflushStrategy = std::make_unique<ConsoleLogStrategy>();
        }

        void EnableFileLogStrategy()
        {
            _fflushStrategy = std::make_unique<FileLogStrategy>();
        }
        //  内部类默认是外部类的友元类,可以访问外部类的私有成员变量
        //  内部类LogMessage,表示一条日志信息的类
        class LogMessage
        {
        public:
            LogMessage(LogLevel &level, std::string &srcName, int lineNum, Logger &logger)
                : _curTime(GetCurTime()),
                  _level(level),
                  _pid(getpid()),
                  _srcName(srcName),
                  _lineNum(lineNum),
                  _logger(logger)
            {
                // 日志的基本信息合并起来
                // std::stringstream用于在内存中进行字符串的输入输出操作, 提供一种方便的方式处理字符串
                // 将不同类型的数据转换为字符串,也可以将字符串解析为不同类型的数据
                std::stringstream ss;
                ss << "[" << _curTime << "] "
                   << "[" << Level2Str(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _srcName << "] "
                   << "[" << _lineNum << "] "
                   << "- ";

                _logInfo = ss.str();
            }

            //  使用模板重载运算符<< -- 支持不同数据类型的输出运算符重载
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ss;
                ss << info;
                _logInfo += ss.str();
                return *this;
            }

            ~LogMessage()
            {
                if (_logger._fflushStrategy)
                {
                    _logger._fflushStrategy->SyncLog(_logInfo);
                }
            }

        private:
            std::string _curTime;   // 日志时间
            LogLevel _level;    // 日志等级
            pid_t _pid; // 进程pid
            std::string _srcName;   // 输出日志的文件名
            int _lineNum;   //输出日志的行号
            std::string _logInfo;   //完整日志内容
            Logger &_logger;    // 方便使用策略进行刷新
        };

        // 使用宏进行替换之后调用的形式如下
        // logger(level, __FILE__, __LINE__) << "hello world" << 3.14;
        // 这里使用仿函数的形式,调用LogMessage的构造函数,构造一个匿名的LogMessage对象
        // 返回的LogMessage对象是一个临时对象,它的生命周期从创建开始到包含它的完整表达式结束(可以简单理解为包含
        // 这个对象的该行代码)
        // 代码调用结束的时候,如果没有LogMessage对象进行临时对象的接收,则会调用析构函数,
        // 如果有LogMessage对象进行临时对象的接收,会调用拷贝构造或者移动构造构造一个对象,并析构临时对象
        // 所以通过临时变量调用析构函数进行日志的打印
        LogMessage operator()(LogLevel level, std::string name, int line)
        {
            return LogMessage(level, name, line, *this);
        }

        ~Logger()
        {
        }

    private:
        std::unique_ptr<LogStrategy> _fflushStrategy;
    };

    //  定义一个全局的Logger对象
    Logger logger;

    // 使用宏定义,简化用户操作并且获取文件名和行号
    #define LOG(level) logger(level, __FILE__, __LINE__) // 使用仿函数的方式进行调用
    #define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
    #define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}

#endif

通过下列方式进行调用

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

using namespace LogModule;

int main()
{
    Enable_Console_Log_Strategy();
    LOG(LogLevel::DEBUG) << "debug";
    LOG(LogLevel::INFO) << "info";
    LOG(LogLevel::WARNING) << "warning";
    LOG(LogLevel::ERROR) << "error";
    LOG(LogLevel::FATAL) << "fatal";
    Enable_File_Log_Strategy();
    LOG(LogLevel::DEBUG) << "hello world";
    LOG(LogLevel::DEBUG) << "hello world";
    LOG(LogLevel::DEBUG) << "hello world";

    return 0;
}

向显示器上打印的部分:

向 my.log 文件中打印的部分

3. 线程池设计

3.1 简单线程池的设计

这里模拟创建一个固定线程数量的线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务。下列线程,日志,条件变量,互斥量等的封装参考前面的博客。

cpp 复制代码
// ThreadPool.hpp

#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include "Log.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Mutex.hpp"

namespace ThreadPoolModule
{
    using namespace ThreadModule;
    using namespace LogModule;
    using namespace CondModule;
    using namespace MutexModule;

    static const int gnum = 5;  // 使用全局变量来表示一个线程池默认的线程数量

    template <typename T>   // 使用模版的方式使线程池支持多类型的任务
    class ThreadPool
    {
    private:
        void WakeUpAllThread()
        {
            if (_sleep_num)
                _cond.Broadcast();

            LOG(LogLevel::DEBUG) << "唤醒所有休眠线程";
        }

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

    public:
        // 创建固定数量的线程
        ThreadPool(int num = gnum)
            : _num(num),
              _isrunning(false),
              _sleep_num(0)
        {
            for (int i = 0; i < _num; i++)
            {
                _threads.emplace_back([this]()
                                      { HandlerTask(); }); // 调用线程的构造函数,线程的构造函数形参是一个回调函数
            }
        }

        void HandlerTask()
        {
            char name[64];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while (true)
            {
                T t;
                // 处理任务
                {
                    LockGuard lockGuard(_mutex);
                    // 1. 队列为空,线程池没有退出,进行休眠
                    while (_taskq.empty() && _isrunning)
                    {
                        _sleep_num++;
                        LOG(LogLevel::INFO) << name << " 进入休眠";
                        _cond.Wait(_mutex);
                        _sleep_num--;
                    }
                    // 2. 任务为空,线程池退出,则该线程退出
                    if (!_isrunning && _taskq.empty())
                    {
                        LOG(LogLevel::INFO) << name << " 退出, 因为线程池退出&&任务队列为空";
                        break;
                    }

                    // 3. 获取任务
                    t = _taskq.front();
                    _taskq.pop();
                }
                t(); // 4. 处理任务
                // LOG(LogLevel::DEBUG) << name << " is running";
            }
        }

        bool Enqueue(const T &in)
        {
            if (_isrunning) // 如果线程池停止,则停止入任务
            {
                LockGuard lockGuard(_mutex);
                _taskq.push(in);
                if (_threads.size() == _sleep_num)  // 如果全部线程都在休眠,则唤醒一个线程
                    WakeOne();
                return true;
            }
            return false;
        }

        void Start()
        {
            if (_isrunning)
                return;
            _isrunning = true;
            for (auto &thread : _threads)
            {
                thread.Start();
            }
        }

        void Stop()
        {
            // 1. 将运行标志位置为false
            LockGuard lockGuard(_mutex);
            if (!_isrunning)
                return;
            _isrunning = false;

            // 2. 唤醒休眠的线程,然后再HandlerTask中进行退出
            WakeUpAllThread();
        }

        void Join()
        {
            for (auto &thread : _threads)
            {
                thread.Join();
                LOG(LogLevel::INFO) << thread.GetName() << " 被Join";
            }
        }

        ~ThreadPool()
        {
        }

    private:
        std::vector<Thread> _threads;
        int _num; // 线程数量
        std::queue<T> _taskq;   // 任务队列
        Cond _cond;
        Mutex _mutex;
        bool _isrunning;
        int _sleep_num;
    };
}
cpp 复制代码
//Task.hpp

#pragma once

#include <iostream>
#include <functional>
#include <unistd.h>
#include "Log.hpp"

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

void Download()
{
    LOG(LogLevel::DEBUG) << "I am a download task...";
    // sleep(3);
}
cpp 复制代码
// Main.cc

#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"

using namespace LogModule;
using namespace ThreadPoolModule;
int main()
{
    Enable_Console_Log_Strategy();
    ThreadPool<task_t> *tp = new ThreadPool<task_t>();
    tp->Start();
    sleep(5);
    int count = 10;
    while (count)
    {
        tp->Enqueue(Download);
        sleep(1);
        count--;
    }
    tp->Stop();
    tp->Join();
    return 0;
}

在主程序中,主线程循环向任务队列中放入下载任务,然后唤醒线程池中休眠的线程进行任务处理。在处理完之后停止线程池,然后对线程池中的线程进行回收。

这里因为任务处理的过程时间很短,所以在下一个任务进入的时候,上一个处理任务的线程处理完任务就进行了休眠,导致打印出来的日志是唤醒一个线程,处理一个任务。当处理任务的时间消耗较长时,线程进入休眠的概率就会下降。

3.2 线程安全的单例模式线程池的设计

3.2.1 单例模式

3.2.1.1 概念

单例模式确保一个类只有一个实例存在,同时提供一个全局访问点, 让其他代码可以方便地获取到这个唯一实例。在很多情况下,我们只需要一个实例来协调系统中的各项操作,比如配置文件管理、数据库连接池、日志记录器等,使用单例模式可以避免多个实例带来的资源浪费和数据不一致问题

3.2.1.2 设计原理

(1)私有构造函数 :单例类的构造函数被设置为私有,防止了外部代码通过 new 关键字来创建该类的实例

(2)静态实例变量:在单例类内部定义一个静态的实例变量,用于保存该类的唯一实例。

(3)静态访问方法 :提供一个静态的公共方法,用于获取该类的唯一实例。在这个方法中,会检查实例是否已经创建,如果未创建则创建一个新实例,若已创建则直接返回该实例。

3.2.2 饿汉实现方式

饿汉式单例在类加载时就创建了实例 ,因此它是线程安全的。但是如果这个实例在程序运行中没有进行使用,就会造成资源的浪费

下列的实现增加了一个 _stop_flag 标记位,在调用 Stop() 时置为1,防止后续在调用其他函数获取单例的时候,调用 Start() 函数重新将 _isrunning 标记位置为 true。在 Start() 中对_stop_flag 标记位进行判断,如果为真,则再次获取单例的时候,不在将 _isrunning 标记位置为true.

cpp 复制代码
// 饿汉式单例模式线程池
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include "Log.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Mutex.hpp"

namespace ThreadPoolModule
{
    using namespace ThreadModule;
    using namespace LogModule;
    using namespace CondModule;
    using namespace MutexModule;

    static const int gnum = 5;  // 使用全局变量来表示一个线程池默认的线程数量

    template <typename T>   // 使用模版的方式使线程池支持多类型的任务
    class ThreadPool
    {
    private:
        void WakeUpAllThread()
        {
            if (_sleep_num)
                _cond.Broadcast();

            LOG(LogLevel::DEBUG) << "唤醒所有休眠线程";
        }

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

        // 私有化构造函数
        ThreadPool(int num = gnum) 
            : _num(num),
              _isrunning(false),
              _sleep_num(0),
              _stop_flag(false)
        {
            for (int i = 0; i < _num; i++)
            {
                _threads.emplace_back([this]()
                                      { HandlerTask(); }); // 调用线程的构造函数,线程的构造函数形参是一个回调函数
            }
        }

        void Start()
        {
            if (_isrunning)
                return;
            if (_stop_flag)
                return;
            _isrunning = true;
            for (auto &thread : _threads)
            {
                thread.Start();
            }
        }

        // 禁用拷贝构造和赋值运算符
        ThreadPool(const ThreadPool<T> &) = delete;
        ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;

    public:
        static ThreadPool<T> *GetInstance()
        {
            inc.Start();
            return &inc;
        }

        void HandlerTask()
        {
            char name[64];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while (true)
            {
                T t;
                // 处理任务
                {
                    LockGuard lockGuard(_mutex);
                    // 1. 队列为空,线程池没有退出,进行休眠
                    while (_taskq.empty() && _isrunning)
                    {
                        _sleep_num++;
                        LOG(LogLevel::INFO) << name << " 进入休眠";
                        _cond.Wait(_mutex);
                        _sleep_num--;
                    }
                    // 2. 任务为空,线程池退出,则该线程退出
                    if (!_isrunning && _taskq.empty())
                    {
                        LOG(LogLevel::INFO) << name << " 退出, 因为线程池退出&&任务队列为空";
                        break;
                    }

                    // 3. 获取任务
                    t = _taskq.front();
                    _taskq.pop();
                }
                t(); // 4. 处理任务
                // LOG(LogLevel::DEBUG) << name << " is running";
            }
        }

        bool Enqueue(const T &in)
        {
            if (_isrunning) // 如果线程池停止,则停止入任务
            {
                LockGuard lockGuard(_mutex);
                _taskq.push(in);
                if (_threads.size() == _sleep_num)  // 如果全部线程都在休眠,则唤醒一个线程
                    WakeOne();
                return true;
            }
            return false;
        }

        void Stop()
        {
            // 1. 将运行标志位置为false
            LockGuard lockGuard(_mutex);
            if (!_isrunning)
                return;
            _isrunning = false;
            _stop_flag = true;

            // 2. 唤醒休眠的线程,然后再HandlerTask中进行退出
            WakeUpAllThread();
        }

        void Join()
        {
            for (auto &thread : _threads)
            {
                thread.Join();
                LOG(LogLevel::INFO) << thread.GetName() << " 被Join";
            }
        }

        ~ThreadPool()
        {
        }

    private:
        std::vector<Thread> _threads;
        int _num; // 线程数量
        std::queue<T> _taskq;   // 任务队列
        Cond _cond;
        Mutex _mutex;
        bool _isrunning;
        int _sleep_num;
        int _stop_flag;

        static ThreadPool<T> inc;  // 单例
    };

    template<typename T>
    ThreadPool<T> ThreadPool<T>::inc;    // 静态成员变量需要在类外进行初始化
}

3.2.3 懒汉实现方式

懒汉式单例在第一次调用静态访问方法时才创建实例,避免了不必要的资源浪费 。不过在多线程环境下,这种实现方式不是线程安全的,可能会创建多个实例。需要通过使用互斥量确保在多线程环境下只会创建一个实例懒汉方式最核心的思想是"延时加载",从而能够优化服务器的启动速度。

在类中增加一个静态互斥量,并在获取单例的时候进行双层判断,保证在多线程场景下使用单例模式的线程池时不会创建多个线程池单例。

cpp 复制代码
// 懒汉式单例模式线程池

#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include "Log.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Mutex.hpp"

namespace ThreadPoolModule
{
    using namespace ThreadModule;
    using namespace LogModule;
    using namespace CondModule;
    using namespace MutexModule;

    static const int gnum = 5; // 使用全局变量来表示一个线程池默认的线程数量

    template <typename T> // 使用模版的方式使线程池支持多类型的任务
    class ThreadPool
    {
    private:
        void WakeUpAllThread()
        {
            if (_sleep_num)
                _cond.Broadcast();

            LOG(LogLevel::DEBUG) << "唤醒所有休眠线程";
        }

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

        // 私有化构造函数
        ThreadPool(int num = gnum)
            : _num(num),
              _isrunning(false),
              _sleep_num(0)
        {
            for (int i = 0; i < _num; i++)
            {
                _threads.emplace_back([this]()
                                      { HandlerTask(); }); // 调用线程的构造函数,线程的构造函数形参是一个回调函数
            }
        }

        void Start()
        {
            if (_isrunning)
                return;
            _isrunning = true;
            for (auto &thread : _threads)
            {
                thread.Start();
            }
        }

        // 禁用拷贝构造和赋值运算符
        ThreadPool(const ThreadPool<T> &) = delete;
        ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;

    public:
        static ThreadPool<T> *GetInstance()
        {
            if (inc == nullptr) // 第一次创建的时候需要加锁,保证创建是原子性的
            {
                LockGuard lockGuard(_gmutex);
                if (inc == nullptr) // 双层判断,保证只会创建一个单例
                {
                    LOG(LogLevel::DEBUG) << "首次使用, 创建单例...";
                    inc = new ThreadPool<T>();
                    inc->Start();
                }
            }
            return inc;
        }

        void HandlerTask()
        {
            char name[64];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while (true)
            {
                T t;
                // 处理任务
                {
                    LockGuard lockGuard(_mutex);
                    // 1. 队列为空,线程池没有退出,进行休眠
                    while (_taskq.empty() && _isrunning)
                    {
                        _sleep_num++;
                        LOG(LogLevel::INFO) << name << " 进入休眠";
                        _cond.Wait(_mutex);
                        _sleep_num--;
                    }
                    // 2. 任务为空,线程池退出,则该线程退出
                    if (!_isrunning && _taskq.empty())
                    {
                        LOG(LogLevel::INFO) << name << " 退出, 因为线程池退出&&任务队列为空";
                        break;
                    }

                    // 3. 获取任务
                    t = _taskq.front();
                    _taskq.pop();
                }
                t(); // 4. 处理任务
                // LOG(LogLevel::DEBUG) << name << " is running";
            }
        }

        bool Enqueue(const T &in)
        {
            if (_isrunning) // 如果线程池停止,则停止入任务
            {
                LockGuard lockGuard(_mutex);
                _taskq.push(in);
                if (_threads.size() == _sleep_num) // 如果全部线程都在休眠,则唤醒一个线程
                    WakeOne();
                return true;
            }
            return false;
        }

        void Stop()
        {
            // 1. 将运行标志位置为false
            LockGuard lockGuard(_mutex);
            if (!_isrunning)
                return;
            _isrunning = false;

            // 2. 唤醒休眠的线程,然后再HandlerTask中进行退出
            WakeUpAllThread();
        }

        void Join()
        {
            for (auto &thread : _threads)
            {
                thread.Join();
                LOG(LogLevel::INFO) << thread.GetName() << " 被Join";
            }
        }

        ~ThreadPool()
        {
        }

    private:
        std::vector<Thread> _threads;
        int _num;             // 线程数量
        std::queue<T> _taskq; // 任务队列
        Cond _cond;
        Mutex _mutex;
        bool _isrunning;
        int _sleep_num;

        static ThreadPool<T> *inc; // 单例指针
        static Mutex _gmutex;      // 用于多线程场景下保护单例不被多次创建
    };

    template <typename T>
    ThreadPool<T> *ThreadPool<T>::inc = nullptr; // 静态成员变量需要在类外进行初始化
    template <typename T>
    Mutex ThreadPool<T>::_gmutex; // 自动调用Mutex的构造函数进行初始化
}
相关推荐
HappRobot2 分钟前
Vim 中替换字符或文本
linux·编辑器·vim
火绒终端安全管理系统10 分钟前
钓鱼网页散播银狐木马,远控后门威胁终端安全
网络·安全·web安全·网络安全·火绒安全
jiunian_cn16 分钟前
【c++】【STL】list详解
数据结构·c++·windows·list·visual studio
Riseandshinexx19 分钟前
8、HTTPD服务--ab压力测试
linux·压力测试
虾球xz22 分钟前
游戏引擎学习第250天:# 清理DEBUG GUID
c++·学习·游戏引擎
我命由我1234524 分钟前
STM32 开发 - stm32f10x.h 头文件(内存映射、寄存器结构体与宏、寄存器位定义、实现点灯案例)
c语言·开发语言·c++·stm32·单片机·嵌入式硬件·嵌入式
xyd陈宇阳30 分钟前
嵌入式开发高频面试题全解析:从基础编程到内存操作核心知识点实战
c语言·数据结构·stm32·算法·面试
淋过很多场雨1 小时前
现代c++获取linux所有的网络接口名称
java·linux·c++
await 4042 小时前
Windows查看和修改IP,IP互相ping通
linux·网络协议·tcp/ip
移远通信2 小时前
2025上海车展 | 移远通信推出自研NG-eCall QuecOpen方案,助力汽车安全新标准加速落地
安全·汽车