简单线程池实现(单例模式)

1.概念

基本概念

线程池是一种多线程处理形式,它预先创建一组线程并管理它们,避免频繁创建和销毁线程带来的性能开销。

在 Linux 环境下,线程池(Thread Pool)是一种常用的并发编程模型,用于复用线程资源 ,避免频繁创建和销毁线程带来的性能开销。它特别适用于高并发、短任务的场景,比如 Web 服务器、数据库连接池、异步任务处理等

为什么需要线程池

  • 降低资源消耗:重复利用已创建的线程
  • 提高响应速度:任务到达时可直接执行,无需等待线程创建
  • 提高线程可管理性:统一分配、调优和监控
  • 防止资源耗尽:避免无限制创建线程导致系统崩溃

✅ 线程池的核心组件

组件 作用说明
任务队列 存放待执行的任务(函数指针、lambda、function 等)
工作线程 从任务队列中取出任务并执行
同步机制 使用互斥锁和条件变量实现线程间通信
线程复用 线程执行完任务后不退出,而是继续等待下一个任务

✅ 线程池的优势

优势 说明
性能提升 避免频繁创建/销毁线程
资源控制 限制最大并发线程数,防止系统过载
任务调度 可配合优先级队列、延迟任务等机制

✅ 线程池的注意事项

问题 解决方案
任务阻塞 避免在线程池中执行阻塞 IO,必要时使用异步 IO
异常处理 任务中抛出的异常不会自动传播,需手动捕获

线程池作用

线程池是一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的应用场景

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

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

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

线程池的种类

a. 创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务接口

b. 浮动线程池,其他同上

此处,我们选择固定线程个数的线程池。

2.实现

前置头文件

cpp 复制代码
thread_pool_test:thread_pool_test.pp
	g++ -o $@ $^ -std=c++17 -lpthread
.PHONY:clean
clean:
	rm -f thread_pool_test
cpp 复制代码
#pragma once

#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"

class Cond
{
public:
    Cond()
    {
        pthread_cond_init(&_cond, nullptr);
    }
    void Wait(Mutex &lock)
    {
        int n = pthread_cond_wait(&_cond, lock.Get());
    }
    void NotifyOne()
    {
        int n = pthread_cond_signal(&_cond);
        (void)n;
    }
    void NotifyAll()
    {
        int n = pthread_cond_broadcast(&_cond);
        (void)n;
    }
    ~Cond()
    {
        pthread_cond_destroy(&_cond);
    }
private:
    pthread_cond_t _cond;
};
cpp 复制代码
#pragma once

#include <iostream>
#include <mutex>
#include <pthread.h>

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }
    pthread_mutex_t *Get()
    {
        return &_lock;
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};

class LockGuard
{
public:
    LockGuard(Mutex *_mutex):_mutexp(_mutex)
    {
        _mutexp->Lock();
    }
    ~LockGuard()
    {
        _mutexp->Unlock();
    }
private:
    Mutex *_mutexp;
};
cpp 复制代码
#pragma once

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

// 规定出场景的日志等级
enum class LogLevel
{
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

// 日志转换成为字符串
std::string Level2String(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";
    }
}

// 根据时间戳,获取可读性较强的时间信息
// 20XX-08-04 12:27:03
std::string GetCurrentTime()
{
    // 1. 获取时间戳
    time_t currtime = time(nullptr);

    // 2. 如何把时间戳转换成为20XX-08-04 12:27:03
    struct tm currtm;
    localtime_r(&currtime, &currtm);

    // 3. 转换成为字符串
    char timebuffer[64];
    snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
             currtm.tm_year + 1900,
             currtm.tm_mon + 1,
             currtm.tm_mday,
             currtm.tm_hour,
             currtm.tm_min,
             currtm.tm_sec);

    return timebuffer;
}

// 策略模式,策略接口
// 1. 刷新的问题 -- 假设我们已经有了一条完整的日志,string->设备(显示器,文件)
// 基类方法
class LogStrategy
{
public:
    // 不同模式核心是刷新方式的不同
    virtual ~LogStrategy() = default;
    virtual void SyncLog(const std::string &logmessage) = 0;
};

// 控制台日志策略,就是日志只向显示器打印,方便我们debug
// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:
    ~ConsoleLogStrategy()
    {
    }
    void SyncLog(const std::string &logmessage) override
    {
        {
            LockGuard lockguard(&_lock);
            std::cout << logmessage << std::endl;
        }
    }

private:
    // 显示器也是临界资源,保证输出线程安全
    Mutex _lock;
};

// 默认路径和日志名称
const std::string logdefaultdir = "log";
const static std::string logfilename = "test.log";

// 文件日志策略
// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:
    // 构造函数,建立出来指定的目录结构和文件结构
    FileLogStrategy(const std::string &dir = logdefaultdir,
                    const std::string filename = logfilename)
        : _dir_path_name(dir), _filename(filename)
    {
        LockGuard lockguard(&_lock);
        if (std::filesystem::exists(_dir_path_name))
        {
            return;
        }
        try
        {
            std::filesystem::create_directories(_dir_path_name);
        }
        catch (const std::filesystem::filesystem_error &e)
        {
            std::cerr << e.what() << "\r\n";
        }
    }
    // 将一条日志信息写入到文件中
    void SyncLog(const std::string &logmessage) override
    {
        {
            LockGuard lockguard(&_lock);
            std::string target = _dir_path_name;
            target += "/";
            target += _filename;
            // 追加方式
            std::ofstream out(target.c_str(), std::ios::app); // append
            if (!out.is_open())
            {
                return;
            }
            out << logmessage << "\n"; // out.write
            out.close();
        }
    }

    ~FileLogStrategy()
    {
    }

private:
    std::string _dir_path_name; // log
    std::string _filename;      // hello.log => log/hello.log
    Mutex _lock;
};

// 具体的日志类
// 1. 定制刷新策略
// 2. 构建完整的日志
class Logger
{
public:
    Logger()
    {
    }
    void EnableConsoleLogStrategy()
    {
        _strategy = std::make_unique<ConsoleLogStrategy>();
    }
    void EnableFileLogStrategy()
    {
        _strategy = std::make_unique<FileLogStrategy>();
    }

    // 内部类,实现RAII风格的日志格式化和刷新
    // 这个LogMessage,表示一条完整的日志对象
    class LogMessage
    {
    public:
        // RAII风格,构造的时候构建好日志头部信息
        LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
            : _curr_time(GetCurrentTime()),
              _level(level),
              _pid(getpid()),
              _filename(filename),
              _line(line),
              _logger(logger)
        {
            // stringstream不允许拷贝,所以这里就当做格式化功能使用
            std::stringstream ss;
            ss << "[" << _curr_time << "] "
               << "[" << Level2String(_level) << "] "
               << "[" << _pid << "] "
               << "[" << _filename << "] "
               << "[" << _line << "]"
               << " - ";
            _loginfo = ss.str();
        }
        // 重载 << 支持C++风格的日志输入,使用模版,表示支持任意类型
        template <typename T>
        LogMessage &operator<<(const T &info)
        {
            std::stringstream ss;
            ss << info;
            _loginfo += ss.str();
            return *this;
        }
        // RAII风格,析构的时候进行日志持久化,采用指定的策略
        ~LogMessage()
        {
            if (_logger._strategy)
            {
                _logger._strategy->SyncLog(_loginfo);
            }
        }

    private:
        std::string _curr_time; // 日志时间
        LogLevel _level;        // 日志等级
        pid_t _pid;             // 进程pid
        std::string _filename;
        int _line;

        std::string _loginfo; // 一条合并完成的,完整的日志信息
        Logger &_logger;      // 引用外部logger类, 方便使用策略进行刷新
    };
    // 故意拷贝,形成LogMessage临时对象,后续在被<<时,会被持续引用,
    // 直到完成输入,才会自动析构临时LogMessage,至此也完成了日志的显示或者刷新
    // 同时,形成的临时对象内包含独立日志数据
    // 未来采用宏替换,进行文件名和代码行数的获取
    LogMessage operator()(LogLevel level, std::string filename, int line)
    {
        return LogMessage(level, filename, line, *this);
    }
    ~Logger()
    {
    }

private:
    // 写入日志的策略
    std::unique_ptr<LogStrategy> _strategy;
};

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

// 使用宏,可以进行代码插入,方便随时获取文件名和行号
#define LOG(level) logger(level, __FILE__, __LINE__)

// 提供选择使用何种日志策略的方法
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()
cpp 复制代码
#pragma once
#include <iostream>
#include <functional>
#include <unistd.h>
#include <sstream>

class Task
{
public:
    Task()
    {}
    Task(int x, int y): a(x), b(y)
    {

    }
    void Execute()
    {
        result = a + b;
    }
    void operator()()
    {
        // sleep(1);
        Execute();
    }
    void Print()
    {
        std::cout << a << " + " << b << " = " << result << std::endl;
    }
    std::string Result2String()
    {
        std::stringstream ss;
        ss << a << " + " << b << " = " << result;
        return ss.str();
    }
private:
    int a;
    int b;
    int result;
};
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <functional>
#include <sys/syscall.h> /* For SYS_xxx definitions */
#include "Logger.hpp"

#define get_lwp_id() syscall(SYS_gettid)

using func_t = std::function<void(const std::string&name)>;
const std::string threadnamedefault = "None-Name";

class Thread
{
public:
    Thread(func_t func, const std::string &name = threadnamedefault)
        : _name(name),
          _func(func),
          _isrunning(false)
    {
        LOG(LogLevel::INFO) << _name << " create thread obj success";
    }
    static void *start_routine(void *args)
    {
        Thread *self = static_cast<Thread *>(args);
        self->_isrunning = true;
        self->_lwpid = get_lwp_id();
        self->_func(self->_name);
        pthread_exit((void *)0);
    }
    void Start()
    {
        int n = pthread_create(&_tid, nullptr, start_routine, this);
        if (n == 0)
        {
            LOG(LogLevel::INFO) << _name << " running success";
        }
    }
    void Stop()
    {
        int n = pthread_cancel(_tid); // 太简单粗暴了
        (void)n;
    }
    // void Die()
    // {
    //     pthread_cancel(_tid);
    // }
    // 检测线程结束并且回收的功能
    void Join()
    {
        if (!_isrunning)
            return;

        int n = pthread_join(_tid, nullptr);
        if (n == 0)
        {
            LOG(LogLevel::INFO) << _name << " pthread_join success";
        }
    }
    ~Thread()
    {
        // LOG(LogLevel::INFO) << _name << " destory thread obj success";
    }

private:
    bool _isrunning;
    pthread_t _tid;
    pid_t _lwpid;
    std::string _name;
    func_t _func;
};

thread_pool

cpp 复制代码
#pragma once
#include <queue>
#include <vector>
#include "Thread.hpp"
#include "Cond.hpp"
#include "Logger.hpp"
using namespace std;

const static int default_thread_num = 5;

template <typename T>
class ThreadPool
{
    void Routine(string name)
    {
        while (true)
        {
            T t;
            {
                LockGuard lock(&_mutex);

                // 如果线程池正在运行且任务队列为空
                // 注意,一定要使用while,防止出错
                while (_is_running && QueueIsEmpty())
                {
                    _wait_thread_num++;
                    _cond.Wait(_mutex); // 线程唤醒后执行的是这里的逻辑   如果队列为空了  就要离开循环了
                    _wait_thread_num--;
                }

                // 如果线程池要退出且任务队列为空
                if (!_is_running && QueueIsEmpty())
                {
                    LOG(LogLevel::INFO) << "线程池准备退出&&任务队列为空 " << name.c_str() << "退出";
                    break;
                }

                // 此时任务队列一定不为空,存在两种情况
                //  1. 线程池准备退出 -- 消耗历史任务
                //  2. 线程池没有准备退出 -- 正常工作
                t = _task_queue.front();
                _task_queue.pop();

                // LOG(LogLevel::DEBUG) << name << "::::" <<_task_queue.size();
                // if (!QueueIsEmpty())
                // {
                //     t = _task_queue.front();
                //      //Linux上面实现的stl库,队列为空的时候(size==0)还可以去数据导致(size--)
                //     //拿到了一个0+0 = 0的任务
                //     //然后size(size_t类型的数据没有负数)就变成了一个非常大的正数,队列就不为空了
                //     //这里段错误和数组的越界访问类似  然后一直拿去数据 触发了段错误
                //     _task_queue.pop();
                // }
            }
            // 此时,线程已经把任务从临界资源获取到线程私有!临界区 -> 线程私有的栈
            // 处理任务时不需要再临界区内部进行,并发进行效率更高
            t(); // 规定,未来的任务必须这样处理!operate()重载
            LOG(LogLevel::DEBUG) << name << " handler task: " << t.Result2String();
        }
    }

public:
    ThreadPool(int threadnum = default_thread_num)
        : _thread_num(threadnum),
          _wait_thread_num(0),
          _is_running(false)
    {
        for (int i = 1; i <= _thread_num; i++)
        {
            // 方法1:
            // auto f = std::bind(hello, this);
            // 方法2
            string name = "thread-" + to_string(i);
            // emplace_back()是STL容器(如vector、deque、list)的成员函数,
            // 用于在容器尾部直接构造元素,避免不必要的拷贝或移动。
            _threads.emplace_back([this](const string &name)
                                  { this->Routine(name); }, name);
        }
        LOG(LogLevel::INFO) << "thread pool obj create success";
    }

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

    void Stop()
    {
        // 线程池要退出,不是立刻就能退出的,它要把任务队列中的任务处理完后才能退出
        if (!_is_running)
            return;
        _is_running = false;
        // 线程池都要退出了,那些休眠的线程还休眠什么,赶紧把他们全部唤醒
        // 处理完任务后线程池好退出
        if (_wait_thread_num)
            _cond.NotifyAll();
    }

    void Enqueue(const T &task)
    {
        // 如果线程池准备退出,任务就不要入队列了
        if (!_is_running)
            return;
        {
            LockGuard lock(&_mutex);
            _task_queue.push(task);
            // LOG(LogLevel::DEBUG) << "一个任务入队列了";
            if (_wait_thread_num)
                _cond.NotifyOne();
        }
    }
    void Wait()
    {
        for (auto &t : _threads)
        {
            t.Join();
        }
        LOG(LogLevel::INFO) << "thread pool wait success";
    }

    bool QueueIsEmpty()
    {
        return _task_queue.empty();
    }

    ~ThreadPool()
    {
    }

private:
    // 任务队列
    queue<T> _task_queue; // 整体使用的临界资源

    vector<Thread> _threads;
    int _thread_num;      // 线程池中线程个数
    int _wait_thread_num; // 线程池正在等待的线程个数

    // 保护线程池安全
    Mutex _mutex;
    Cond _cond;

    // 检测线程池是否在运行
    bool _is_running;
};
cpp 复制代码
#include"ThreadPool.hpp"
#include "Task.hpp"
#include<time.h>
#include<memory>
int main()
{
    srand(time(nullptr));
    //使用控制台策略,向显示器输出日志
    EnableConsoleLogStrategy();
    unique_ptr<ThreadPool<Task>> tp=make_unique<ThreadPool<Task>>();
    tp->Start();
    int cnt=10;
    while(cnt--)
    {
        int x=rand()%9+1;
        int y=rand()%9+1;
        Task task(x,y);
        tp->Enqueue(task);
        sleep(1);
    }
    tp->Stop();
    tp->Wait();
    return 0;
}

注意若是if{...}在while{...}前面就会出错

如下:

3 线程池单例模式

3.1 概念

单例模式确保一个类只有一个实例,并提供一个全局访问点。

线程池使用单例模式的理由:

  1. 系统中通常只需要一个全局的线程池
  2. 避免资源浪费(多个线程池竞争系统资源)
  3. 便于统一管理和监控

3.2 饿汉实现方式和懒汉实现方式

洗碗的例子

吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.

吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.

懒汉方式最核心的思想是 "延时加载". 从而能够优化服务器的启动速度

核心区别:

特性 饿汉式 (Eager) 懒汉式 (Lazy)
初始化时机 程序启动时/类加载时 第一次使用时
线程安全 天然线程安全 需要额外同步机制
资源占用 启动时即占用资源 使用时才占用资源
性能 启动慢,运行时快 启动快,第一次使用稍慢
实现复杂度 简单 较复杂

3.2.1 饿汉实现方式

核心思想

"提前准备,立即加载" - 在程序启动或类加载时就创建实例。

cpp 复制代码
template <typename T>
class Singleton {
static T data;//静态全局变量定义在全局数据区,编译后就会为它分配空间
public:
    static T* GetInstance()
    {
        return &data;
    }
};

只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例.

饿汉式特点分析

优点:

  1. 线程安全:无需考虑多线程同步问题
  2. 性能好:获取实例时直接返回,无锁开销
  3. 实现简单:代码简洁,不易出错

缺点:

  1. 启动时间:可能增加程序启动时间
  2. 资源浪费:即使不使用也占用资源
  3. 初始化顺序:不同编译单元间的静态变量初始化顺序不确定

适用场景:

  • 单例初始化开销小
  • 单例在程序运行中必定会被使用
  • 对性能要求极高,不能忍受锁开销
  • 多线程环境且不想处理同步问题

3.2.2 懒汉实现方式

核心思想

"用时创建,延迟加载" - 在第一次请求时才创建实例。

cpp 复制代码
template <typename T>
class Singleton {
static T* inst;
public:
    static T* GetInstance()
    {
        if (inst == NULL) {
        inst = new T();
        }
        return inst;
    }
};

存在一个严重的问题, 线程不安全.

第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例.

但是后续再次调用, 就没有问题了

懒汉式特点分析

优点:

  1. 延迟加载:只有使用时才创建,节省资源
  2. 启动快速:不增加程序启动时间
  3. 灵活性高:可以根据需要动态创建

缺点:

  1. 线程安全复杂:需要处理多线程同步
  2. 首次访问慢:第一次获取实例需要创建和同步
  3. 实现复杂:需要考虑各种边界情况

适用场景:

  • 单例初始化开销大
  • 单例可能不会被使用
  • 系统资源有限,需要按需加载
  • 对启动时间敏感的应用

3.2.3 深度对比与分析

内存模型分析
cpp 复制代码
// 饿汉式内存布局
// 编译时确定,在数据段分配内存
// 程序加载时即初始化

// 懒汉式内存布局
// 运行时确定,在堆上分配内存
// 第一次访问时初始化
线程安全性对比
实现方式 饿汉式 懒汉式
基础实现 线程安全 线程不安全
加锁实现 不需要 需要锁机制
双重检查 不需要 需要DCLP
现代实现 总是安全 call_once或局部静态变量
选择指南

选择饿汉式的情况:

  1. 单例的创建和运行时开销非常小
  2. 程序运行过程中一定会使用该单例
  3. 希望避免任何锁开销
  4. 单例的初始化不依赖其他未初始化的资源

选择懒汉式的情况:

  1. 单例初始化成本高(连接数据库、读取大文件等)
  2. 单例可能在整个程序生命周期中都不被使用
  3. 单例的初始化依赖运行时信息或其他单例
  4. 需要动态控制单例的创建时机

3.2.4 总结

特性 饿汉式 懒汉式
哲学 积极准备 按需分配
实现 简单直接 复杂但灵活
线程安全 天然安全 需额外保障
资源使用 启动时占用 使用时占用
性能特点 启动慢使用快 启动快第一次慢
现代推荐 静态成员或局部静态变量 局部静态变量(call_once)

最佳实践建议:

  1. 优先使用基于局部静态变量的懒汉式(C++11及以上)
  2. 如果确定单例一定被使用且初始化快,可以考虑饿汉式
  3. 在多线程环境中,避免手写双重检查锁定,使用标准库提供的线程安全机制
  4. 考虑是否真的需要单例,评估是否有更好的设计模式替代

3.3 懒汉方式实现单例模式

其他的头文件都和上面一样

cpp 复制代码
ThreadPool:ThreadPool.cpp
	g++ -o $@ $^ -std=c++17 -lpthread
.PHONY:clean
clean:
	rm -f ThreadPool
cpp 复制代码
#pragma once
#include <queue>
#include <vector>
#include "Thread.hpp"
#include "Cond.hpp"
#include "Logger.hpp"
using namespace std;

// 单例线程池 - 懒汉模式
const static int default_thread_num = 3;

template <typename T>
class ThreadPool
{
    void Routine(string name)
    {
        while (true)
        {
            T t;
            {
                LockGuard lock(&_mutex);

                // 如果线程池正在运行且任务队列为空
                // 注意,一定要使用while,防止出错
                while (_is_running && QueueIsEmpty())
                {
                    _wait_thread_num++;
                    _cond.Wait(_mutex); // 线程唤醒后执行的是这里的逻辑   如果队列为空了  就要离开循环了
                    _wait_thread_num--;
                }

                // 如果线程池要退出且任务队列为空
                if (!_is_running && QueueIsEmpty())
                {
                    LOG(LogLevel::INFO) << "线程池准备退出&&任务队列为空 " << name.c_str() << "退出";
                    break;
                }

                // 此时任务队列一定不为空,存在两种情况
                //  1. 线程池准备退出 -- 消耗历史任务
                //  2. 线程池没有准备退出 -- 正常工作
                t = _task_queue.front();
                _task_queue.pop();

                // LOG(LogLevel::DEBUG) << name << "::::" <<_task_queue.size();
                // if (!QueueIsEmpty())
                // {
                //     t = _task_queue.front();
                //      //Linux上面实现的stl库,队列为空的时候(size==0)还可以去数据导致(size--)
                //     //拿到了一个0+0 = 0的任务
                //     //然后size(size_t类型的数据没有负数)就变成了一个非常大的正数,队列就不为空了
                //     //这里段错误和数组的越界访问类似  然后一直拿去数据 触发了段错误
                //     _task_queue.pop();
                // }
            }
            // 此时,线程已经把任务从临界资源获取到线程私有!临界区 -> 线程私有的栈
            // 处理任务时不需要再临界区内部进行,并发进行效率更高
            t(); // 规定,未来的任务必须这样处理!operate()重载
            LOG(LogLevel::DEBUG) << name << " handler task: " << t.Result2String();
        }
    }

private:
    // 将构造函数设为私有,不允许用户直接创建对象
    ThreadPool(int threadnum = default_thread_num)
        : _thread_num(threadnum),
          _wait_thread_num(0),
          _is_running(false)
    {
        for (int i = 1; i <= _thread_num; i++)
        {
            // 方法1:
            // auto f = std::bind(hello, this);
            // 方法2
            string name = "thread-" + to_string(i);
            // emplace_back()是STL容器(如vector、deque、list)的成员函数,
            // 用于在容器尾部直接构造元素,避免不必要的拷贝或移动。
            _threads.emplace_back([this](const string &name)
                                  { this->Routine(name); }, name);
        }
        LOG(LogLevel::INFO) << "thread pool obj create success";
    }
    // 禁掉拷贝构造和赋值重载
    ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
    ThreadPool(const ThreadPool<T> &) = delete;

public:
    void Start()
    {
        if (_is_running)
            return;
        _is_running = true;
        for (auto &t : _threads)
            t.Start();
    }

    void Stop()
    {
        // 线程池要退出,不是立刻就能退出的,它要把任务队列中的任务处理完后才能退出
        if (!_is_running)
            return;
        _is_running = false;
        // 线程池都要退出了,那些休眠的线程还休眠什么,赶紧把他们全部唤醒
        // 处理完任务后线程池好退出
        if (_wait_thread_num)
            _cond.NotifyAll();
    }

    void Enqueue(const T &task)
    {
        // 如果线程池准备退出,任务就不要入队列了
        if (!_is_running)
            return;
        {
            LockGuard lock(&_mutex);
            _task_queue.push(task);
            // LOG(LogLevel::DEBUG) << "一个任务入队列了";
            if (_wait_thread_num)
                _cond.NotifyOne();
        }
    }
    void Wait()
    {
        for (auto &t : _threads)
        {
            t.Join();
        }
        LOG(LogLevel::INFO) << "thread pool wait success";
    }

    bool QueueIsEmpty()
    {
        return _task_queue.empty();
    }

    // 用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针
    // 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针
    // 静态成员函数只能访问静态成员,非静态的成员函数,可以访问任意的静态成员变量和静态成员函数

    // 地址转字符串,证明多线程申请的单例都是同一个
    static std::string ToHex(ThreadPool<T> *addr)
    {
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%p", addr);
        return buffer;
    }
    // 获取单例
    static ThreadPool<T> *GetInstance()
    {
        // A, B, c
        // 线程安全,提高效率式的获取单例
        // 双重 if 判定, 避免不必要的锁竞争
        if (!_instance)
        {
            // 外层if是为了防止获取单例后多线程申请单例时判断前还要申请锁,降低效率
            //  保证第二次之后,所有线程,不用在加锁,直接返回_instance单例对象
            LockGuard lockguard(&_singleton_lock);
            if (!_instance)
            {
                _instance = new ThreadPool<T>();
                LOG(LogLevel::DEBUG) << "线程池单例首次被使用,创建并初始化, addr: " << ToHex(_instance);

                _instance->Start();
            }
        }
        else
        {
            LOG(LogLevel::DEBUG) << "线程池单例已经存在,直接获取, addr: " << ToHex(_instance);
        }
        return _instance;
    }

    ~ThreadPool()
    {
    }

private:
    // 任务队列
    queue<T> _task_queue; // 整体使用的临界资源

    vector<Thread> _threads;
    int _thread_num;      // 线程池中线程个数
    int _wait_thread_num; // 线程池正在等待的线程个数

    // 保护线程池安全
    Mutex _mutex;
    Cond _cond;

    // 检测线程池是否在运行
    bool _is_running;

    // 单例中静态指针
    // 需要设置 volatile 关键字, 防止被编译器优化.
    // volatile static ThreadPool<T> *_instance;

    // 用static修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化。
    // 类内定义,类外初始化

    // 添加单例模式
    static ThreadPool<T> *_instance;
    static Mutex _singleton_lock;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;

template <class T>
Mutex ThreadPool<T>::_singleton_lock;
cpp 复制代码
#include "Task.hpp"
#include "ThreadPool.hpp"
#include <memory>
#include <time.h>

int main()
{
    srand(time(nullptr) ^ getpid());
    EnableConsoleLogStrategy();
    //编译时会报错,单例模式不允许用户直接创建线程池对象
    // std::unique_ptr<ThreadPool<Task>> tp = std::make_unique<ThreadPool<Task>>(10);
    // tp->Start();

    int cnt = 10;
    while (cnt--)
    {
        // 生产任务
        int x = rand() % 10 + 1;
        usleep(rand() % 73);
        int y = rand() % 5 + 1;
        Task t(x, y);

        // push到线程池中,处理

        // 突破类域就可以访问静态成员,可以通过 类名::静态成员(不需要对象就能调用)
        // 或者 对象.静态成员 来访问静态成员变量和静态成员函数
        ThreadPool<Task>::GetInstance()->Enqueue(t);
        sleep(1);
    }

    ThreadPool<Task>::GetInstance()->Stop();

    ThreadPool<Task>::GetInstance()->Wait();

    return 0;
}
相关推荐
import_random6 小时前
[环境变量]export命令的作用是什么
linux
何妨呀~6 小时前
Linux在VMware上添加磁盘与扩展分区
linux·运维·服务器
墨雪不会编程6 小时前
C++基础语法篇八 ——【类型转换、再探构造、友元】
java·开发语言·c++
CIb0la6 小时前
Linux 6.19-rc1 释出,龙芯为内核加入 32 位架构支持
linux·运维
牛奶咖啡136 小时前
Linux常见系统故障案例说明并修复解决(上)
linux·linux云计算·如何恢复linux中误删的数据·linux数据删除后的解决方法·分析修复linux无法启动故障·分析修复系统配置错误故障·linux系统资源配置错误修复
老毛肚6 小时前
登录架构设计
java·开发语言
南棱笑笑生6 小时前
20251215给飞凌OK3588-C开发板适配Rockchip原厂的Buildroot【linux-6.1】系统时统计eth1的插拔次数
linux·c语言·开发语言·rockchip
yuuki2332336 小时前
【C++】内存管理
java·c++·算法
model20056 小时前
Web 服务和 SFTP 用户 操作目录
linux·运维·服务器