【Linux】30.Linux 多线程(4)

文章目录

  • [9. 线程池](#9. 线程池)
    • [9.1 日志与策略模式](#9.1 日志与策略模式)
    • [9.2 线程安全的单例模式](#9.2 线程安全的单例模式)
      • [9.2.1 什么是单例模式](#9.2.1 什么是单例模式)
      • [9.2.2 单例模式的特点](#9.2.2 单例模式的特点)
      • [9.2.3 饿汉实现方式和懒汉实现方式](#9.2.3 饿汉实现方式和懒汉实现方式)
      • [9.2.4 饿汉方式实现单例模式](#9.2.4 饿汉方式实现单例模式)
      • [9.2.5 懒汉方式实现单例模式](#9.2.5 懒汉方式实现单例模式)
      • [9.2.6 懒汉方式实现单例模式(线程安全版本)](#9.2.6 懒汉方式实现单例模式(线程安全版本))
    • [9.3 线程池设计](#9.3 线程池设计)
      • [9.2.1 线程池](#9.2.1 线程池)
      • [9.2.2 对线程做一个简单的封装-Thread.hpp](#9.2.2 对线程做一个简单的封装-Thread.hpp)
      • [9.2.3 单例模式进程池](#9.2.3 单例模式进程池)

9. 线程池

9.1 日志与策略模式

  1. 什么是设计模式?

    大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是设计模式。

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

日志格式以下几个指标是必须得有的:

  1. 时间戳
  2. 日志等级
  3. 日志内容

以下几个指标是可选的:

  1. 文件名行号
  2. 进程,线程相关id信息等

日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采用自定义日志的方式。


9.2 线程安全的单例模式

9.2.1 什么是单例模式

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


9.2.2 单例模式的特点

某些类, 只应该具有一个对象(实例), 就称之为单例。

例如一个男人只能有一个媳妇。

在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中。此时往往要用一个单例的类来管理这些数据。

单例模式主要有两种实现方式:懒汉式和饿汉式。


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

洗碗的例子

  1. 吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
  2. 吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.

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


9.2.4 饿汉方式实现单例模式

cpp 复制代码
template <typename T>
class Singleton {
    static T data;
    public:
    static T* GetInstance() {
        return &data;
    }
};

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


9.2.5 懒汉方式实现单例模式

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

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

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

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


9.2.6 懒汉方式实现单例模式(线程安全版本)

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;
    }
};

注意事项:

  1. 加锁解锁的位置
  2. 双重 if 判定, 避免不必要的锁竞争
  3. volatile关键字防止过度优化

9.3 线程池设计

线程池:

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

  • 需要大量的线程来完成任务,且完成任务的时间比较短。 比如WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  • 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  • 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。
    线程池的种类
  1. 创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务接口
  2. 浮动线程池,其他同上

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


9.2.1 线程池

ThreadPool.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>

// 线程信息结构体,保存线程ID和名称
struct ThreadInfo {
    pthread_t tid;        // 线程ID
    std::string name;     // 线程名称
};

// 默认线程池中线程数量
static const int defalutnum = 5;

template <class T>
class ThreadPool {
public:
    // 互斥锁操作封装
    void Lock() {
        // 对互斥锁加锁,如果锁已被占用则阻塞等待
        pthread_mutex_lock(&mutex_);  
    }

    void Unlock() {
        // 解锁互斥锁,释放锁的占用
        pthread_mutex_unlock(&mutex_);
    }

    // 条件变量操作封装
    // 唤醒一个正在等待该条件变量的线程
    void Wakeup() {
        // 唤醒一个正在等待该条件变量的线程
        // 如果有多个线程等待,则随机唤醒一个
        pthread_cond_signal(&cond_);//去任务队列唤醒
    }

    //线程休眠
    void ThreadSleep() {
        // 使当前线程在条件变量上等待
        // 1. 自动释放互斥锁mutex_
        // 2. 线程进入睡眠状态
        // 3. 当被唤醒时,自动重新获取互斥锁
        pthread_cond_wait(&cond_, &mutex_);  
        /*
        参数和返回值:
            参数1 cond:条件变量的指针
            参数2 mutex:互斥锁的指针
        返回值:成功返回0,失败返回错误码
        */
    }

    // 判断任务队列是否为空
    bool IsQueueEmpty() {
        // 返回任务队列的空状态
        return tasks_.empty();
    }

    // 根据线程ID获取线程名称
    std::string GetThreadName(pthread_t tid) {
        // 遍历线程信息数组
        for (const auto &ti : threads_) {
            // 找到匹配的线程ID
            if (ti.tid == tid)
                return ti.name;  // 返回对应的线程名
        }
        return "None";  // 未找到则返回"None"
    }

public:
    ThreadPool(int num = defalutnum) : threads_(num) {
        pthread_mutex_init(&mutex_, nullptr);    // 初始化互斥锁
        /*
        参数说明:
            mutex: 指向 pthread_mutex_t 类型的指针,用于存储要初始化的互斥锁
            attr: 指向 pthread_mutexattr_t 类型的指针,用于指定互斥锁的属性
                传入 nullptr 表示使用默认属性
                默认属性为:快速互斥锁(PTHREAD_MUTEX_FAST_NP)
        返回值:
            成功返回 0
            失败返回错误码
        */
        pthread_cond_init(&cond_, nullptr);      // 初始化条件变量
        /*
        参数说明:
            cond: 指向 pthread_cond_t 类型的指针,用于存储要初始化的条件变量
            attr: 指向 pthread_condattr_t 类型的指针,用于指定条件变量的属性
                传入 nullptr 表示使用默认属性
                默认属性为:进程私有(PTHREAD_PROCESS_PRIVATE)
        返回值:
            成功返回 0
            失败返回错误码
        */
    }


    // 1.启动线程池
    void Start() {
        // 获取线程数量(线程池大小)
        int num = threads_.size();
        
        // 循环创建工作线程
        for (int i = 0; i < num; i++) {
            // 为每个线程设置名称,格式为 "thread-1", "thread-2" 等
            // std::to_string 是 C++ 标准库提供的函数,用于将数值类型转换为字符串
            threads_[i].name = "thread-" + std::to_string(i + 1);
            
            // 创建线程
            pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
            /*
            int pthread_create(
                pthread_t *thread,          // 输出参数,存储新创建的线程ID
                const pthread_attr_t *attr, // 线程属性,nullptr表示使用默认属性
                void *(*start_routine)(void*), // 线程函数
                void *arg                   // 传递给线程函数的参数
            );
            */
        }
    }

    // 工作线程的处理函数
    static void* HandlerTask(void* args) {
        // 将 void* 的参数args转换回线程池指针
        ThreadPool<T>* tp = static_cast<ThreadPool<T>*>(args);
        //static_cast<>用于进行编译时类型转换。
        //语法: static_cast<新类型>(表达式)
        //ThreadPool<T>*表示指向ThreadPool<T>类型对象的指针

        // 获取当前线程的名称
        // pthread_self() 返回当前线程ID
        std::string name = tp->GetThreadName(pthread_self());
        
        // 工作线程的主循环
        while (true) {
            // 先加锁:准备访问共享资源(任务队列)
            tp->Lock();
            
            // 当任务队列为空时,进入等待状态
            while (tp->IsQueueEmpty()) {
                tp->ThreadSleep();  // 条件变量等待
            }
            
            // 从任务队列取出一个任务
            T t = tp->Pop();
            
            // 取完任务后解锁:不再访问共享资源,让其他线程可以取任务
            tp->Unlock();

            // 执行任务,它调用了任务对象的 operator() 运算符,也就是实际执行任务的代码
            t();//(不需要加锁,因为任务已经安全地取出)
            
            // 输出任务执行结果
            std::cout << name << " run, " << "result: " << t.GetResult() << std::endl;
        }
    }

    // 向任务队列添加任务
    void Push(const T& t) {
        // 加锁保护任务队列
        Lock();  

        // 将任务添加到队列末尾
        tasks_.push(t);
        Wakeup();  // 唤醒一个等待的工作线程来处理新任务,使用pthread_cond_signal通知

        Unlock();  // 解锁,允许其他线程访问任务队列
    }

    // 从任务队列获取任务
    T Pop() {
        T t = tasks_.front();
        tasks_.pop();
        return t;
    }

    ~ThreadPool() {
        pthread_mutex_destroy(&mutex_);    // 销毁互斥锁
        pthread_cond_destroy(&cond_);      // 销毁条件变量
    }


private:
    std::vector<ThreadInfo> threads_;  // 工作线程集合
    std::queue<T> tasks_;             // 任务队列

    pthread_mutex_t mutex_;           // 任务队列互斥锁
    pthread_cond_t cond_;            // 任务队列条件变量

    static ThreadPool<T>* tp_;        // 单例指针
    static pthread_mutex_t lock_;     // 单例锁
};

// 静态成员初始化
template <class T>
ThreadPool<T>* ThreadPool<T>::tp_ = nullptr;

template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;

Task.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>

// 定义所有支持的运算符集合
std::string opers="+-*/%";  // 加、减、乘、除、取模

// 定义错误码枚举类型
enum {
    DivZero=1,    // 除零错误
    ModZero,      // 取模零错误(值为2)
    Unknown       // 未知错误(值为3)
};

// Task类:表示一个算术运算任务
class Task
{
public:
    // 默认构造函数
    Task()
    {}

    // 带参数的构造函数
    // x: 第一个操作数
    // y: 第二个操作数
    // op: 运算符
    Task(int x, int y, char op) 
        : data1_(x),      // 初始化第一个操作数
          data2_(y),      // 初始化第二个操作数
          oper_(op),      // 初始化运算符
          result_(0),     // 初始化结果为0
          exitcode_(0)    // 初始化错误码为0(表示无错误)
    {
    }

    // 执行运算的核心函数
    void run()
    {
        switch (oper_)
        {
        case '+':    // 加法运算
            result_ = data1_ + data2_;
            break;
        case '-':    // 减法运算
            result_ = data1_ - data2_;
            break;
        case '*':    // 乘法运算
            result_ = data1_ * data2_;
            break;
        case '/':    // 除法运算
            {
                if(data2_ == 0) exitcode_ = DivZero;  // 检查除数为0的情况
                //如果是0则设置错误代码为DivZero(除零错误)

                else result_ = data1_ / data2_;
            }
            break;
        case '%':    // 取模运算
           {
                if(data2_ == 0) exitcode_ = ModZero;  // 检查除数为0的情况
                else result_ = data1_ % data2_;
            }            
            break;
        default:     // 未知运算符
            exitcode_ = Unknown;
            break;
        }
    }

    // 重载函数调用运算符"()",使对象可以像函数一样被调用
    void operator ()()
    {
        run();
    }

    // 获取完整的运算结果字符串
    // 格式:操作数1 运算符 操作数2 = 结果 [错误码]
    std::string GetResult()
    {
        std::string r = std::to_string(data1_);  // 转换第一个操作数
        r += oper_;                              // 添加运算符
        r += std::to_string(data2_);            // 转换第二个操作数
        r += "=";                               // 添加等号
        r += std::to_string(result_);           // 添加计算结果
        r += "[code: ";                         // 添加错误码标记
        r += std::to_string(exitcode_);         // 添加错误码
        r += "]";
        return r;
    }

    // 获取任务的字符串表示(不包含结果)
    // 格式:操作数1 运算符 操作数2 =?
    std::string GetTask()
    {
        std::string r = std::to_string(data1_);  // 转换第一个操作数
        r += oper_;                              // 添加运算符
        r += std::to_string(data2_);            // 转换第二个操作数
        r += "=?";                              // 添加待计算标记
        return r;
    }

    // 析构函数(目前为空)
    ~Task()
    {
    }

private:
    int data1_;     // 第一个操作数
    int data2_;     // 第二个操作数
    char oper_;     // 运算符

    int result_;    // 计算结果
    int exitcode_;  // 错误码(0表示无错误)
};

Main.cc

cpp 复制代码
#include <iostream>
#include <ctime>
#include "ThreadPool.hpp"   // 线程池头文件
#include "Task.hpp"         // 任务类头文件

int main()
{
    // 直接创建进程池
    ThreadPool<Task> *tp = new ThreadPool<Task>(5);  // 创建5个线程的线程池
    tp->Start();
    
    srand(time(nullptr) ^ getpid());

    // 主循环:持续生成任务
    while(true)
    {
        // 1. 构建任务
        int x = rand() % 10 + 1;    // 生成1-10的随机数作为第一个操作数
        usleep(10);                 // 短暂休眠,增加随机数的离散性
        int y = rand() % 5;         // 生成0-4的随机数作为第二个操作数
        // 从操作符数组中随机选择一个操作符
        char op = opers[rand()%opers.size()];

        // 创建任务对象
        Task t(x, y, op);
        // 将任务提交给线程池
        tp->Push(t);
        
        // 2. 交给线程池处理
        // 输出任务信息,表示主线程创建了什么任务
        std::cout << "main thread make task: " << t.GetTask() << std::endl;

        // 休眠1秒,控制任务生成速率
        sleep(1);
    }
}

9.2.2 对线程做一个简单的封装-Thread.hpp

Thread.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <time.h>
#include <pthread.h>

// 定义函数指针类型,用于存储线程回调函数
// 无参数、无返回值的函数指针
typedef void (*callback_t)(); //callback_t 和 void (*)()等价

// 用于生成线程名称的静态计数器
static int num = 1;

class Thread
{
public:
    // 静态线程入口函数(pthread_create的要求)
    // args: 传入的参数(Thread对象指针)
    // 返回值:线程退出时的返回值
    static void* Routine(void* args)
    {
        Thread* thread = static_cast<Thread*>(args);  // 将void*转换回Thread*
        thread->Entery();  // 调用实际的线程入口函数
        return nullptr;
    }

public:
    // 构造函数
    // cb: 线程要执行的回调函数
    Thread(callback_t cb) // callback_t cb等价于 void (*cb)()
        : tid_(0),              // 线程ID初始化为0
          name_(""),            // 线程名初始化为空
          start_timestamp_(0),  // 启动时间戳初始化为0
          isrunning_(false),    // 运行状态初始化为false
          cb_(cb)               // 设置回调函数
          // 表示将构造函数参数 cb 赋值给成员变量 cb_
    {}

    // 析构函数
    ~Thread()
    {}

    // 启动线程
    void Run()
    {
        name_ = "thread-" + std::to_string(num++);  // 生成线程名称
        start_timestamp_ = time(nullptr);           // 记录启动时间戳
        isrunning_ = true;                         // 设置运行状态为true
        // 创建线程,传入this指针作为参数
        pthread_create(&tid_, nullptr, Routine, this);
        /*
            int pthread_create(
                pthread_t *thread,          // 输出参数,存储新创建的线程ID
                const pthread_attr_t *attr, // 线程属性,nullptr表示使用默认属性
                void *(*start_routine)(void*), // 线程函数
                void *arg                   // 传递给线程函数的参数
            );
        */
    }

    // 等待线程结束
    void Join()
    {
        pthread_join(tid_, nullptr);  // 等待线程结束
        /*
        int pthread_join(pthread_t thread, void **retval);
        参数说明:
            thread: 要等待的线程的线程ID(pthread_t类型)
            retval: 用于存储线程的返回值的指针。如果不关心返回值,可以传入nullptr
        返回值:
            成功返回0
            失败返回错误码
        */
        isrunning_ = false;          // 设置运行状态为false
    }

    // 获取线程名称
    std::string Name()
    {
        return name_;
    }

    // 获取线程启动时间戳
    uint64_t StartTimestamp()
    {
        return start_timestamp_;
    }

    // 获取线程运行状态
    bool IsRunning()
    {
        return isrunning_;
    }

    // 实际的线程入口函数
    void Entery()
    {
        cb_();  // 执行回调函数
    }

private:
    pthread_t tid_;             // 线程ID
    std::string name_;          // 线程名称
    uint64_t start_timestamp_;  // 启动时间戳
    bool isrunning_;            // 运行状态标志

    callback_t cb_;             // 回调函数指针
    //等价于 void (*cb)()
};

Main.cc

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"     // 引入自定义的线程类

using namespace std;

// 测试用的线程函数
// 无限循环打印信息,每秒一次
void Print()
{
    while(true)
    {
        printf("haha, 我是一个封装的线程...\n");
        sleep(1);    // 休眠1秒
    }
}

int main()
{
    Thread t(Print);    // 创建单个线程对象
    t.Run();           // 启动线程
    cout << "是否启动成功: " << t.IsRunning() << endl;      // 检查线程是否正在运行
    cout << "启动成功时间戳: " << t.StartTimestamp() << endl; // 获取线程启动时间戳
    cout << "线程的名字: " << t.Name() << endl;              // 获取线程名称
    t.Join();          // 等待线程结束(已注释)

    return 0;
}

9.2.3 单例模式进程池

Task.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>

// 定义所有支持的运算符集合
std::string opers="+-*/%";  // 加、减、乘、除、取模

// 定义错误码枚举类型
enum {
    DivZero=1,    // 除零错误
    ModZero,      // 取模零错误(值为2)
    Unknown       // 未知错误(值为3)
};

// Task类:表示一个算术运算任务
class Task
{
public:
    // 默认构造函数
    Task()
    {}

    // 带参数的构造函数
    // x: 第一个操作数
    // y: 第二个操作数
    // op: 运算符
    Task(int x, int y, char op) 
        : data1_(x),      // 初始化第一个操作数
          data2_(y),      // 初始化第二个操作数
          oper_(op),      // 初始化运算符
          result_(0),     // 初始化结果为0
          exitcode_(0)    // 初始化错误码为0(表示无错误)
    {
    }

    // 执行运算的核心函数
    void run()
    {
        switch (oper_)
        {
        case '+':    // 加法运算
            result_ = data1_ + data2_;
            break;
        case '-':    // 减法运算
            result_ = data1_ - data2_;
            break;
        case '*':    // 乘法运算
            result_ = data1_ * data2_;
            break;
        case '/':    // 除法运算
            {
                if(data2_ == 0) exitcode_ = DivZero;  // 检查除数为0的情况
                //如果是0则设置错误代码为DivZero(除零错误)

                else result_ = data1_ / data2_;
            }
            break;
        case '%':    // 取模运算
           {
                if(data2_ == 0) exitcode_ = ModZero;  // 检查除数为0的情况
                else result_ = data1_ % data2_;
            }            
            break;
        default:     // 未知运算符
            exitcode_ = Unknown;
            break;
        }
    }

    // 重载函数调用运算符"()",使对象可以像函数一样被调用
    void operator ()()
    {
        run();
    }

    // 获取完整的运算结果字符串
    // 格式:操作数1 运算符 操作数2 = 结果 [错误码]
    std::string GetResult()
    {
        std::string r = std::to_string(data1_);  // 转换第一个操作数
        r += oper_;                              // 添加运算符
        r += std::to_string(data2_);            // 转换第二个操作数
        r += "=";                               // 添加等号
        r += std::to_string(result_);           // 添加计算结果
        r += "[code: ";                         // 添加错误码标记
        r += std::to_string(exitcode_);         // 添加错误码
        r += "]";
        return r;
    }

    // 获取任务的字符串表示(不包含结果)
    // 格式:操作数1 运算符 操作数2 =?
    std::string GetTask()
    {
        std::string r = std::to_string(data1_);  // 转换第一个操作数
        r += oper_;                              // 添加运算符
        r += std::to_string(data2_);            // 转换第二个操作数
        r += "=?";                              // 添加待计算标记
        return r;
    }

    // 析构函数(目前为空)
    ~Task()
    {
    }

private:
    int data1_;     // 第一个操作数
    int data2_;     // 第二个操作数
    char oper_;     // 运算符

    int result_;    // 计算结果
    int exitcode_;  // 错误码(0表示无错误)
};

ThreadPool.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>

// 线程信息结构体,保存线程ID和名称
struct ThreadInfo {
    pthread_t tid;        // 线程ID
    std::string name;     // 线程名称
};

// 默认线程池中线程数量
static const int defalutnum = 5;

template <class T>
class ThreadPool {
public:
    // 互斥锁操作封装
    void Lock() {
        // 对互斥锁加锁,如果锁已被占用则阻塞等待
        pthread_mutex_lock(&mutex_);  
    }

    void Unlock() {
        // 解锁互斥锁,释放锁的占用
        pthread_mutex_unlock(&mutex_);
    }

    // 条件变量操作封装
    // 唤醒一个正在等待该条件变量的线程
    void Wakeup() {
        // 唤醒一个正在等待该条件变量的线程
        // 如果有多个线程等待,则随机唤醒一个
        pthread_cond_signal(&cond_);//去任务队列唤醒
    }

    //线程休眠
    void ThreadSleep() {
        // 使当前线程在条件变量上等待
        // 1. 自动释放互斥锁mutex_
        // 2. 线程进入睡眠状态
        // 3. 当被唤醒时,自动重新获取互斥锁
        pthread_cond_wait(&cond_, &mutex_);  
        /*
        参数和返回值:
            参数1 cond:条件变量的指针
            参数2 mutex:互斥锁的指针
        返回值:成功返回0,失败返回错误码
        */
    }

    // 判断任务队列是否为空
    bool IsQueueEmpty() {
        // 返回任务队列的空状态
        return tasks_.empty();
    }

    // 根据线程ID获取线程名称
    std::string GetThreadName(pthread_t tid) {
        // 遍历线程信息数组
        for (const auto &ti : threads_) {
            // 找到匹配的线程ID
            if (ti.tid == tid)
                return ti.name;  // 返回对应的线程名
        }
        return "None";  // 未找到则返回"None"
    }

public:

    // 1.启动线程池
    void Start() {
        // 获取线程数量(线程池大小)
        int num = threads_.size();
        
        // 循环创建工作线程
        for (int i = 0; i < num; i++) {
            // 为每个线程设置名称,格式为 "thread-1", "thread-2" 等
            // std::to_string 是 C++ 标准库提供的函数,用于将数值类型转换为字符串
            threads_[i].name = "thread-" + std::to_string(i + 1);
            
            // 创建线程
            pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
            /*
            int pthread_create(
                pthread_t *thread,          // 输出参数,存储新创建的线程ID
                const pthread_attr_t *attr, // 线程属性,nullptr表示使用默认属性
                void *(*start_routine)(void*), // 线程函数
                void *arg                   // 传递给线程函数的参数
            );
            */
        }
    }

    // 工作线程的处理函数
    static void* HandlerTask(void* args) {
        // 将 void* 的参数args转换回线程池指针
        ThreadPool<T>* tp = static_cast<ThreadPool<T>*>(args);
        //static_cast<>用于进行编译时类型转换。
        //语法: static_cast<新类型>(表达式)
        //ThreadPool<T>*表示指向ThreadPool<T>类型对象的指针

        // 获取当前线程的名称
        // pthread_self() 返回当前线程ID
        std::string name = tp->GetThreadName(pthread_self());
        
        // 工作线程的主循环
        while (true) {
            // 先加锁:准备访问共享资源(任务队列)
            tp->Lock();
            
            // 当任务队列为空时,进入等待状态
            while (tp->IsQueueEmpty()) {
                tp->ThreadSleep();  // 条件变量等待
            }
            
            // 从任务队列取出一个任务
            T t = tp->Pop();
            
            // 取完任务后解锁:不再访问共享资源,让其他线程可以取任务
            tp->Unlock();

            // 执行任务,它调用了任务对象的 operator() 运算符,也就是实际执行任务的代码
            t();//(不需要加锁,因为任务已经安全地取出)
            
            // 输出任务执行结果
            std::cout << name << " run, " << "result: " << t.GetResult() << std::endl;
        }
    }

    // 向任务队列添加任务
    void Push(const T& t) {
        // 加锁保护任务队列
        Lock();  

        // 将任务添加到队列末尾
        tasks_.push(t);
        Wakeup();  // 唤醒一个等待的工作线程来处理新任务,使用pthread_cond_signal通知

        Unlock();  // 解锁,允许其他线程访问任务队列
    }

    // 从任务队列获取任务
    T Pop() {
        T t = tasks_.front();
        tasks_.pop();
        return t;
    }

    // 获取线程池单例
    static ThreadPool<T>* GetInstance() {
        // 第一次检查:不加锁
        if (nullptr == tp_) {  // 双检锁模式的第一次检查
            pthread_mutex_lock(&lock_);// 加锁
            
            // 第二次检查:加锁后再次验证
            if (nullptr == tp_) {  // 双检锁模式的第二次检查
                std::cout << "log: singleton create done first!" << std::endl;
                tp_ = new ThreadPool<T>();
                // 创建一个新的ThreadPool对象并返回指向该对象的指针,将新创建的ThreadPool对象的地址赋值给tp_
            }
            pthread_mutex_unlock(&lock_);// 解锁
        }
        return tp_;
        /*
        只用一次检查且不加锁:多个线程同时进入if语句,会创建多个实例。
        直接加锁:每次获取实例都要加锁,即使实例已经创建好了,性能很差。
--------------------------------------------------------------------------
 第一次调用GetInstance时:
        Thread1                    Thread2
        检查tp_为空               检查tp_为空
        获得锁                    等待锁
        再次检查tp_为空
        创建实例
        释放锁                    获得锁
                                检查tp_不为空
                                直接返回
--------------------------------------------------------------------------
后续调用GetInstance时:
        Thread1                    Thread2
        检查tp_不为空             检查tp_不为空
        直接返回                  直接返回
--------------------------------------------------------------------------
        */
    }

private:
    // 构造函数(私有)
    // 1. 声明为私有构造函数,禁止外部直接创建对象,保证单例模式
    // 2. 参数 num 指定线程池中线程的数量,默认值为 defalutnum(5个线程)
    // 3. 使用初始化列表 threads_(num) 创建指定数量的线程信息存储空间
    // 4. 初始化互斥锁和条件变量,用于任务队列的线程同步
    ThreadPool(int num = defalutnum) : threads_(num) {
        pthread_mutex_init(&mutex_, nullptr);    // 初始化互斥锁
        /*
        参数说明:
            mutex: 指向 pthread_mutex_t 类型的指针,用于存储要初始化的互斥锁
            attr: 指向 pthread_mutexattr_t 类型的指针,用于指定互斥锁的属性
                传入 nullptr 表示使用默认属性
                默认属性为:快速互斥锁(PTHREAD_MUTEX_FAST_NP)
        返回值:
            成功返回 0
            失败返回错误码
        */
        pthread_cond_init(&cond_, nullptr);      // 初始化条件变量
        /*
        参数说明:
            cond: 指向 pthread_cond_t 类型的指针,用于存储要初始化的条件变量
            attr: 指向 pthread_condattr_t 类型的指针,用于指定条件变量的属性
                传入 nullptr 表示使用默认属性
                默认属性为:进程私有(PTHREAD_PROCESS_PRIVATE)
        返回值:
            成功返回 0
            失败返回错误码
        */
    }

    // 析构函数(私有)
    // 1. 声明为私有析构函数,禁止外部直接删除对象
    // 2. 负责清理线程池资源,销毁互斥锁和条件变量
    // 3. 由于使用单例模式,析构函数通常不会被调用(程序结束时系统自动清理)
    ~ThreadPool() {
        pthread_mutex_destroy(&mutex_);    // 销毁互斥锁
        pthread_cond_destroy(&cond_);      // 销毁条件变量
    }

    // 禁止拷贝构造和赋值
    // 1. 使用 delete 关键字删除拷贝构造函数和赋值运算符
    // 2. 保证线程池对象不能被复制,维护单例模式的唯一性
    // 3. 防止因拷贝导致的资源多重释放问题
    ThreadPool(const ThreadPool<T>&) = delete;              // 禁用拷贝构造函数
    const ThreadPool<T>& operator=(const ThreadPool<T>&) = delete;  // 禁用赋值运算符

private:
    std::vector<ThreadInfo> threads_;  // 工作线程集合
    std::queue<T> tasks_;             // 任务队列

    pthread_mutex_t mutex_;           // 任务队列互斥锁
    pthread_cond_t cond_;            // 任务队列条件变量

    static ThreadPool<T>* tp_;        // 单例指针
    static pthread_mutex_t lock_;     // 单例锁
};

// 静态成员初始化
template <class T>
ThreadPool<T>* ThreadPool<T>::tp_ = nullptr;
//nullptr 将单例指针tp_初始化为空指针

template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
//PTHREAD_MUTEX_INITIALIZER 是一个宏,用于初始化互斥锁为默认属性

Main.cpp

cpp 复制代码
#include <iostream>
#include <ctime>
#include "ThreadPool.hpp"   // 线程池头文件
#include "Task.hpp"         // 任务类头文件

int main()
{
    // 注释提出的问题:如果获取单例对象的时候,也是多线程获取的呢?
    // 这个问题涉及到单例模式的线程安全性
    // 需要在GetInstance()中使用双检锁或其他线程安全的实现方式

    // 启动提示信息
    std::cout << "process runn..." << std::endl;
    sleep(3);    // 程序启动后等待3秒,方便观察启动过程

    // 两种创建线程池的方式:
    // 1. 直接创建(已注释):
    // ThreadPool<Task> *tp = new ThreadPool<Task>(5);  // 创建5个线程的线程池
    
    // 2. 使用单例模式(当前使用):
    ThreadPool<Task>::GetInstance()->Start();  // 获取线程池实例并启动

    // 初始化随机数种子
    // 使用当前时间和进程ID异或,增加随机性
    srand(time(nullptr) ^ getpid());

    // 主循环:持续生成任务
    while(true)
    {
        // 1. 构建任务
        int x = rand() % 10 + 1;    // 生成1-10的随机数作为第一个操作数
        usleep(10);                 // 短暂休眠,增加随机数的离散性
        int y = rand() % 5;         // 生成0-4的随机数作为第二个操作数
        // 从操作符数组中随机选择一个操作符
        char op = opers[rand()%opers.size()];

        // 创建任务对象
        Task t(x, y, op);
        // 将任务提交给线程池
        ThreadPool<Task>::GetInstance()->Push(t);
        
        // 2. 交给线程池处理
        // 输出任务信息,表示主线程创建了什么任务
        std::cout << "main thread make task: " << t.GetTask() << std::endl;

        // 休眠1秒,控制任务生成速率
        sleep(1);
    }
}
相关推荐
kunge201335 分钟前
Ubuntu22.04 安装virtualbox7.1
linux·virtualbox
清溪54936 分钟前
DVWA中级
linux
m0_748254091 小时前
2025最新华为云国际版注册图文流程-不用绑定海外信用卡注册
服务器·数据库·华为云
MUY09901 小时前
应用控制技术、内容审计技术、AAA服务器技术
运维·服务器
楠奕1 小时前
elasticsearch8.12.0安装分词
运维·jenkins
Sadsvit1 小时前
源码编译安装LAMP架构并部署WordPress(CentOS 7)
linux·运维·服务器·架构·centos
xiaok1 小时前
为什么 lsof 显示多个 nginx 都在 “使用 443”?
linux
java资料站2 小时前
Jenkins
运维·jenkins
苦学编程的谢2 小时前
Linux
linux·运维·服务器
G_H_S_3_2 小时前
【网络运维】Linux 文本处理利器:sed 命令
linux·运维·网络·操作文本