【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);
    }
}
相关推荐
云梦谭1 分钟前
ubuntu server环境下使用mitmproxy代理
linux·mitmproxy
yqcoder2 分钟前
centos 和 ubuntu 区别
linux·ubuntu·centos
黑客老李30 分钟前
一次使用十六进制溢出绕过 WAF实现XSS的经历
java·运维·服务器·前端·sql·学习·xss
圆️️31 分钟前
【故障处理】ORA-19849 ORA-19612 0RA-17627 ORA-03114
运维·数据库·oracle
爱北的琳儿43 分钟前
CentOS7清理大文件(/dev/vda1几乎接近于满状态)
运维·服务器
圆️️1 小时前
12c及以后 ADG主备切换
服务器·网络·数据库
阿里云云原生1 小时前
云消息队列 ApsaraMQ Serverless 演进:高弹性低成本、更稳定更安全、智能化免运维
运维·安全·serverless
一个高效工作的家伙1 小时前
安装mariadb+galera搭建数据库集群
运维·服务器·数据库
web147862107235 小时前
SQL Server详细使用教程(包含启动SQL server服务、建立数据库、建表的详细操作) 非常适合初学者
服务器·数据库·oracle
能源革命7 小时前
MongoDB-7.0.15安装(CentOS7)
运维·数据库·mongodb