Linux多线程之生产消费模型,日志版线程池

文章目录

生产消费模型

多执行流并发的模型,在这种模型中,一定会存在一个临界资源,并且有线程向临界资源内存放数据,我们将存放数据的线程称为生产者,有线程从临界资源内读取数据,我们称之为消费者。若是临界资源内为空,则消费者在读取时需要阻塞等待,若为满,则生产者在存放数据时也要阻塞等待。

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而 通过容器来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给容器,消费者不找生产者 要数据,而是直接从容器里取,容器就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个容器就是用来给生产者和消费者解耦的。从而达到解耦,支持并发,支持忙闲不均。

总结一下,就是有以下特点:

  • 一个交易场所(特定数据结构形式存在的一段内存空间)

  • 两种角色(生产角色,消费角色),生产线程和消费线程

  • 三种关系,生产和生产,消费和消费,生产和消费,这三种方式都是互斥关系。但是生产和消费之间还要维持同步关系

真正提高效率的并不是在阻塞队列中加入或者获取任务这一过程,而是在实践场景中,生产任务和消费任务(执行任务)往往会占据大部分的时间,此时我们可以多设置生产者和消费者,这样就可以让任务在生产和执行的时间段实现高并发,从而提高整体效率。但是如果生产任务和执行任务所占据的时间少,那么则不需要多生产多消费,我们可以总结出以下使用场景:

  • 生产快,消费快:单生产单消费
  • 生产快,消费慢:单生产多消费
  • 生产慢,消费快:多生产单消费
  • 生产慢,消费慢:多生产多消费

一句话概括就是,耗时间的就让多个线程去并发处理

基于BlocKQueue的生产者消费模型

设计BlockQueue

BlockQueue是一种特殊的队列,称为阻塞队列,他符合我们生产消费模型所需的场所特点

push时,队列为满则阻塞,等待pop出数据后有了空间在push。需要设置大小,来进行判断是否为满,该大小可以动态分配

pop时,队列为空则阻塞,等待有新push的数据在pop

因此我们可以使用queue来适配出一个BlockQueue。除此之外,我们是在多线程场景中使用阻塞队列,还要保证线程安全,加入互斥锁可以保证线程安全,使得任意时刻只有一个线程访问阻塞队列,同时,我们还要保证生产消费的同步性,使用条件变量来保证同步,在队列为空的时候阻塞消费者,唤醒生产者,在队列为满的时候阻塞生产者,唤醒消费者。

工作流程可以如下表示:

同时,要避免出现伪唤醒的情况。

什么是伪唤醒

以阻塞队列的插入函数为例:

cpp 复制代码
 void push(const T &data)
​    {
​      	pthread_mutex_lock(&_mutex);
​     	 if(IsFull())
​      	 { 
​      		  pthread_cond_wait(&_product_cond, &_mutex);
​     	 }
​      	_que.push(data);
   	    pthread_mutex_unlock(&_mutex);
​        pthread_cond_signal(&_consumer_cond);
​    }

在进入函数后首先加锁进行保护线程安全,方式多个线程同时访问队列。

然后进行队列容量判断,若为满,则需要阻塞等待,等待结束后或者不为满就进行数据push,然后释放锁,唤醒其他线程。

但是,若条件变量的阻塞等待出现异常,即pthread_cond_wait(&_product_cond, &_mutex);函数调用异常返回,或者使用了其他定时的条件变量,此时尚未满足队列不为空的条件,而生产者却被唤醒,代码会继续执行,若进行push数据则会出现更大的问题。

那么怎么解决呢?

只需把判断条件if改成while即可

​ while(IsFull())

​ {

​ pthread_cond_wait(&_product_cond, &_mutex);

​ }

即使出现了异常唤醒或者提前唤醒,那他也会再次检查条件,不满足则继续等待,满足则继续执行,使得在push的时候队列一定是不满的。

代码示例

​ BlockQueue.hpp

cpp 复制代码
#include <pthread.h>
#include <queue>

namespace MyBqueue
{
    const int gcapacity = 5;  // 默认队列容量
	template <class T>
    class BlockQueue
    {
    private:
        // 判断队列是否已满	
        bool IsFull()
        {
            return _capacity == _que.size();
        }

        // 判断队列是否为空
        bool IsEmpty()
        {
            return _que.size() == 0;
        }

    public:
        // 构造函数,初始化队列容量和互斥锁,条件变量
        BlockQueue(int capacity = gcapacity) : _capacity(capacity)
        {
            pthread_mutex_init(&_mutex, nullptr);        // 初始化互斥锁
            pthread_cond_init(&_consumer_cond, nullptr); // 初始化消费者条件变量
            pthread_cond_init(&_product_cond, nullptr);  // 初始化生产者条件变量
        }

        // 向队列中添加元素(生产者操作)
        void push(const T &data)
        {
            pthread_mutex_lock(&_mutex);  // 加锁保护临界区

            // 如果队列已满,生产者等待,这里使用while防止伪唤醒
            while (IsFull())
            {
                pthread_cond_wait(&_product_cond, &_mutex);
            }
            _que.push(data);  // 添加数据到队列

            pthread_mutex_unlock(&_mutex);      // 释放锁
            pthread_cond_signal(&_consumer_cond); // 唤醒等待的消费者
        }

        // 从队列中取出元素(消费者操作)
        const T &pop()
        {
            pthread_mutex_lock(&_mutex);  // 加锁保护临界区

            // 如果队列为空,消费者等待,这里使用while防止伪唤醒
            while (IsEmpty())
            {
                pthread_cond_wait(&_consumer_cond, &_mutex);
            }

            const T &ret = _que.front();  // 获取队首元素
            _que.pop();                   // 移除队首元素

            pthread_mutex_unlock(&_mutex);     // 释放锁
            pthread_cond_signal(&_product_cond); // 唤醒等待的生产者

            return ret;  // 返回取出的元素
        }

        // 析构函数,清理资源
        ~BlockQueue()
        {
            pthread_mutex_destroy(&_mutex);
            pthread_cond_destroy(&_product_cond);
            pthread_cond_destroy(&_consumer_cond);
        }

    private:
        std::queue<T> _que;        // 底层队列容器
        int _capacity;             // 队列容量

        pthread_mutex_t _mutex;    // 互斥锁,保护队列操作
        pthread_cond_t _consumer_cond;  // 消费者条件变量
        pthread_cond_t _product_cond;   // 生产者条件变量
    };
}

main.cpp

cpp 复制代码
#include "BlockQueue.hpp"
#include <pthread.h>
#include <iostream>
#include <unistd.h>

static int cnt = 100;  // 生产计数器

// 生产者线程函数
void *product(void *ptr)
{
    // 将参数转换为阻塞队列指针
    MyBqueue::BlockQueue<std::string>* p = (MyBqueue::BlockQueue<std::string>*)ptr;
    
    while(cnt--)  // 生产100个数据
    {
        p->push(std::to_string(cnt));  // 将数字转换为字符串并推入队列
        usleep(50000);  // 生产间隔50ms
    }
    return nullptr;
}

// 消费者线程函数
void *consumer(void *ptr)
{
    MyBqueue::BlockQueue<std::string>* p = (MyBqueue::BlockQueue<std::string>*)ptr;
    std::string tmp;
    
    while(true)  // 持续消费
    {
        tmp = p->pop();  // 从队列中取出数据
        std::cout << gettid() << ":" << tmp << std::endl;  // 打印线程ID和消费的数据
        usleep(10000);  // 消费间隔10ms
    }
    return nullptr;
}

int main()
{
    // 创建容量为20的阻塞队列
    MyBqueue::BlockQueue<std::string>* ptr = new MyBqueue::BlockQueue<std::string>(20);
    
    pthread_t c1, c2, c3;    // 生产者线程ID(实际只用了c1),想要几个线程就初始化几个线程即可
    pthread_t c4, c5, c6;    // 消费者线程ID

    // 创建1个生产者线程
    pthread_create(&c1, nullptr, product, ptr);
    
    // 创建3个消费者线程
    pthread_create(&c4, nullptr, consumer, ptr);
    pthread_create(&c5, nullptr, consumer, ptr);
    pthread_create(&c6, nullptr, consumer, ptr);

    // 等待线程结束(注意:这里等待了未创建的线程c2、c3,这是bug)
    pthread_join(c1, nullptr);

    pthread_join(c4, nullptr);
    pthread_join(c5, nullptr);
    pthread_join(c6, nullptr);
    
    
    delete ptr;

    return 0;
}

基于环形队列和信号量的生产消费模型

除了阻塞队列之外,我们还可以使用环形队列和信号量来实现生产消费模型。

环形队列

与普通队列不同,环形队列的容量大小固定,队尾的下一个元素是队头,我们可以使用一个vector来模拟实现环形队列,

对于上述状态的环形队列,我们对其进行pop操作

pop();

pop();

pop();

在que中对应的操作为

head++;

head++;

head++;

之后的结果如下图所示:

然后再进行插入

push(7);

push(8);

push(9);

此时,对应que操作为:

que[end] = 7; end++; //end = 7

que[end] = 8; end++; //end = 8 ,超过了que容量,但是我们需要此时等于0

que[end] = 9; end++; // end = 9,超过了容量,我们需要此时等于1

此时,问题出现了,end++会大于que容量,从而造成越界,那么既然我们是环形队列,只需让end在超出容量的时候从0再次开始即可

我们只需在每次下标自增操作的时候对其模上容量即可,那么就需要固定大小,从而进行摸操作

改正后操作:

que[end] = 7; end++; end%=8;

que[end] = 8; end++; end%=8;

que[end] = 9; end++; end%=8;

结果如下:

同理,在pop操作时,对于head也要进行该操作。

这样我们就可以使用vecot来模拟出一个环形队列

那么怎么判空和判满呢

可以注意到,当对列为空的时候 ,head == end,但是当队列为满的时候也是 head == end,那么就会产生二义性,对此有两种解决方法

  1. 引入计数器来计数,计算当前有效数据个数

  2. 牺牲一个空间,end + 1 == head的时候就是满,end == head的时候为空

此时,我们已经可以实现一个功能齐全的环形队列了。

引入信号量

POSIX信号量 , 与SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。

信号量是一个计数器,本质上是对资源的一种预定机制,他采用原子操作将内部资源的使用状态让我们在外部就可以判断出来从而不用我们自己去判断是否是否满足访问条件,我们可以使用该"计数器"来搭配环形队列实现生产消费模型。

初始化和销毁信号量

c 复制代码
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
//参数:  pshared:0表示线程间共享,非零表示进程间共享  value:信号量初始值
int sem_destroy(sem_t *sem);
//销毁信号量

功能接口

c 复制代码
int sem_wait(sem_t *sem); 
//相当于P操作,等待信号量,会将信号量的值减1,等于0了就就会阻塞
int sem_post(sem_t *sem);
//相当于V操作,可以归还资源了。将信号量值加1,等于设定的初始值就会阻塞

如果将临界资源整体使用,就相当于整个资源只有一份,将信号量的初始值设为1,这就称为一个二元信号量,二元信号量就等同于互斥锁

综合设计模型

我们使用环形队列充当场所的话,那么可以认为生产者所需要的是空间资源,他将生产出来的数据放进空间后形成数据,此时空间资源转化为数据资源,而消费者所需的就是数据资源,消费后又重新变为空间资源。

因此我们引入两个信号量,一个信号量用于生产者之间空间资源使用的同步,一个信号量用于消费者之间数据资源的同步。

并且空间资源的减少必定会有数据资源的增加,数据资源的减少也必定有空间资源的增加。

代码示例

SurrondQueue.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <pthread.h>
#include <vector>
#include <semaphore.h>

const int gdefaultcap = 5; // 默认队列容量

template <class T>
class SurrondQueue
{
private:
    // 信号量P操作(等待)
    void P(sem_t &sem)
    {
        sem_wait(&sem);
    }
    
    // 信号量V操作(通知)
    void V(sem_t &sem)
    {
        sem_post(&sem);
    }
    
    // 互斥锁加锁
    void Lock(pthread_mutex_t &mutex)
    {
        pthread_mutex_lock(&mutex);
    }
    
    // 互斥锁解锁
    void UnLock(pthread_mutex_t &mutex)
    {
        pthread_mutex_unlock(&mutex);
    }

public:
    // 构造函数,初始化环形队列
    SurrondQueue(int max_cap = gdefaultcap)
        : _max_cap(max_cap),           // 设置最大容量
          _suque(max_cap, T()),        // 初始化vector,大小为max_cap,用T的默认构造函数填充
          _pos_push(0),                // 生产者位置初始化为0
          _pos_pop(0)                  // 消费者位置初始化为0
    {
        // 初始化消费者信号量,初始值为0(开始时没有数据资源)
        sem_init(&_consume_sem, 0, 0);
        // 初始化生产者信号量,初始值为_max_cap(开始时有max_cap个空间资源)
        sem_init(&_product_sem, 0, _max_cap);
        // 初始化消费者互斥锁
        pthread_mutex_init(&_consume_mutex, nullptr);
        // 初始化生产者互斥锁
        pthread_mutex_init(&_product_mutex, nullptr);
    }
    
    // 析构函数,清理资源
    ~SurrondQueue()
    {
        sem_destroy(&_consume_sem);
        sem_destroy(&_product_sem);
        pthread_mutex_destroy(&_consume_mutex);
        pthread_mutex_destroy(&_product_mutex);
    }

    // 生产者向队列中添加数据
    void push(const T &in)
    {
        P(_product_sem);           // P操作申请空间资源(有空位才能生产)
        Lock(_product_mutex);      // 加生产者锁
        
        _suque[_pos_push] = in;    // 将数据放入队列
        _pos_push++;               // 移动生产者位置
        _pos_push %= _max_cap;     // 环形队列,取模回到起点
        
        UnLock(_product_mutex);    // 解生产者锁
        V(_consume_sem);           // V操作增加数据资源(有新数据可消费)
    }

    // 消费者从队列中取出数据
    void pop(T *const out)
    {
        P(_consume_sem);           // P操作申请数据资源(有数据才可消费)
        Lock(_consume_mutex);      // 加消费者锁
        
        *out = _suque[_pos_pop];   // 从队列取出数据
        _pos_pop++;                // 移动消费者位置
        _pos_pop %= _max_cap;      // 环形队列,取模回到起点
        
        UnLock(_consume_mutex);    // 解消费者锁
        V(_product_sem);           // V操作增加空间资源(有新空间可生产)
    }

private:
    std::vector<T> _suque;        // 环形队列容器
    int _pos_push;                // 生产者位置指针
    int _pos_pop;                 // 消费者位置指针
    int _max_cap;                 // 队列最大容量

    sem_t _consume_sem;           // 消费者信号量(数据资源信号量)
    sem_t _product_sem;           // 生产者信号量(空间资源信号量)

    pthread_mutex_t _consume_mutex;  // 消费者互斥锁(保护消费者操作)
    pthread_mutex_t _product_mutex;  // 生产者互斥锁(保护生产者操作)
};

main.cpp

cpp 复制代码
#include "SurrondQueue.hpp"
#include <iostream>
#include <pthread.h>
#include <unistd.h>

// 生产者线程函数
void* product(void* ptr)
{
    // 将void*指针转换为SurrondQueue<int>*类型
    SurrondQueue<int>* p = static_cast<SurrondQueue<int>*>(ptr);
    int cnt = 0;  // 生产的数据计数器
    
    while(true)
    {
        p->push(cnt);  // 将数据放入环形队列
        std::cout << gettid() << " deliver:" << cnt << std::endl;  // 打印生产信息
        cnt++;         // 计数器递增
        usleep(100000); // 休眠100ms(模拟生产耗时)
    }
    return nullptr;
}

// 消费者线程函数
void* consumer(void* ptr)
{
    // 将void*指针转换为SurrondQueue<int>*类型
    SurrondQueue<int>* p = static_cast<SurrondQueue<int>*>(ptr);
    int cnt = 0;  // 存储消费的数据
    
    while(true)
    {
        p->pop(&cnt);  // 从环形队列取出数据
        std::cout << gettid() << " get:" << cnt << std::endl;  // 打印消费信息
        usleep(300000); // 休眠300ms(模拟消费耗时)
    }
    return nullptr;
}

int main()
{
    // 创建环形队列对象
    SurrondQueue<int>* ptr = new SurrondQueue<int>;
    
    // 定义线程变量
    pthread_t p1, p2;     // 生产者线程
    pthread_t c1, c2, c3; // 消费者线程

    // 创建1个生产者线程
    pthread_create(&p1, nullptr, product, ptr);
    // 可以创建第二个生产者线程(当前被注释掉了)
    // pthread_create(&p2, nullptr, product, ptr);

    // 创建3个消费者线程
    pthread_create(&c1, nullptr, consumer, ptr);
    pthread_create(&c2, nullptr, consumer, ptr);
    pthread_create(&c3, nullptr, consumer, ptr);

    // 等待线程结束(实际上这些线程都是无限循环,不会结束)
    pthread_join(p1, nullptr);
    pthread_join(c1, nullptr);

	delete ptr;

    
    return 0;
}

与阻塞队列不同的是,环形队列在生产与生产,消费与消费之间单独加锁,使用两把锁,生产与消费间并没有加锁,这样做可以让环形队列在push的同时也pop,因为是环形队列,如果head和end的位置不在同一个位置,那么就不处于同一空间,也自然不会出现数据不一致,从而再次提升整体的并发效率。

而阻塞队列只使用了一把锁,这把锁保护着队列,是的无论是谁都不可能在同一时刻同时访问队列,因为阻塞队列底层我们使用了queue,我们无法确定stl中queue是如何设计,无法保证线程安全,因此使用互斥锁进行绝对的保护。

线程池

日志

软件运行的记录信息,可以向显示器或者文件进行打印,必须有特定的格式

比如:[日志等级] [pid] [filename] [filenumber] [time] 日志内容(支持可变参数)

日志等级:DEBUG,INFO, WARNING ,ERROR,FATAL(致命的)

因此我们可以写一个日志类,使得我们在打印信息的时候以同一风格去打印,加入时间文件等参数也更方便我们去查找问题和代码维护。

日志的模块化分层设计

在日志中我们使用了RAII技术实现自动锁管理:

LockGurard.hpp

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

class LockGuard
{
public:
    LockGuard()
    {
        pthread_mutex_init(&_mutex, nullptr);
    }
    ~LockGuard()
    {
        pthread_mutex_destroy(&_mutex);
    }

private:
    pthread_mutex_t _mutex;
};

在构造的时候加锁,在析构的时候释放锁,这种设计确保:

  • 异常安全:即使发生异常也能正确释放锁
  • 作用域控制:锁的生命周期与函数调用周期一致
  • 避免死锁:自动管理避免了手动加解锁可能出现的错误

我们的日志系统可以采用三层架构:

  • 数据层 (logmessage类):专门负责存储日志的元数据,包括级别、进程信息、文件位置、时间戳和实际消息内容。这种分离使得数据结构和处理逻辑解耦。
cpp 复制代码
 // 日志级别枚举
    enum grade
    {
        DEBUG,   // 调试信息
        INFO,    // 普通信息
        WARRING, // 警告信息
        ERROR,   // 错误信息
        FARAL    // 严重错误信息
    };

    // 日志消息类 - 封装单条日志的所有信息
    class logmessage
    {
    public:
        int _grade;           // 日志级别
        pid_t _pid;           // 进程ID
        std::string _filename; // 文件名
        int _filenumber;      // 行号
        std::string _current_time; // 当前时间
        std::string _message; // 日志内容
    };
  • 处理层 (Log类):包含所有日志处理的核心逻辑:

    • 格式化:将原始数据转换为统一的日志格式
    • 时间处理:生成标准化的时间戳
    • 输出路由:根据配置决定输出目标
    • 线程安全:通过锁机制保证并发安全
    cpp 复制代码
      // 输出类型定义
    #define SCREEN_TYPE 1  // 输出到屏幕
    #define FILE_TYPE 2    // 输出到文件
        
        // 默认日志文件路径
        std::string glogfile = "./log.txt";
    
        // 主日志类
        class Log
        {
        private:
            // 输出到屏幕
            void ShowToScreen(const std::string &show)
            {
                std::cout << show;
            }
            
            // 输出到文件
            void ShowToFile(const std::string &show)
            {
                std::ofstream out("log.txt", std::ios::app); // 以追加模式打开文件
    
                if (!out.is_open())
                {
                    return; // 文件打开失败直接返回
                }
                out << show; // 写入日志
                out.close(); // 关闭文件
            }
    
            // 获取当前时间字符串
            std::string GetCurrentTime()
            {
                time_t now = time(nullptr);
                struct tm *str_now = localtime(&now);
                char buffer[1024];
                snprintf(buffer, sizeof(buffer),
                         "%d-%02d-%02d %02d:%02d:%02d", // 格式: 年-月-日 时:分:秒
                         str_now->tm_year + 1900,  // 年份需要加1900
                         str_now->tm_mon + 1,      // 月份需要加1
                         str_now->tm_mday,         // 日
                         str_now->tm_hour,         // 时
                         str_now->tm_min,          // 分
                         str_now->tm_sec);         // 秒
                return buffer;
            }
            
            // 将日志级别枚举转换为字符串
            std::string GetGradeString(int greade)
            {
                switch (greade)
                {
                case 0:
                    return "DEBUG";
                case 1:
                    return "INFO";
                case 2:
                    return "WARRING";
                case 3:
                    return "ERROR";
                case 4:
                    return "FATAL";
                default:
                    return "UNKONW";
                }
            }
            
            // 格式化日志消息
            std::string GetFormatMessage(logmessage *logmes)
            {
                char buffer[2048];
                // 格式: [级别][进程ID][文件名][行号][时间] 消息内容
                snprintf(buffer, sizeof(buffer),
                         "[%s][%d][%s][%d][%s] %s",
                         GetGradeString(logmes->_grade).c_str(), // 日志级别字符串
                         logmes->_pid,                           // 进程ID
                         logmes->_filename.c_str(),              // 文件名
                         logmes->_filenumber,                    // 行号
                         logmes->_current_time.c_str(),          // 时间
                         logmes->_message.c_str());              // 消息内容
                return buffer;
            }
    
            // 显示日志到指定目标
            void ShowLog(logmessage *logmes)
            {
                std::string final_message = GetFormatMessage(logmes);
                switch (_type)
                {
                case SCREEN_TYPE:
                    ShowToScreen(final_message); // 输出到屏幕
                    break;
                case FILE_TYPE:
                    ShowToFile(final_message);   // 输出到文件
                    break;
                }
            }
    
        public:
            // 构造函数
            Log(int type = SCREEN_TYPE, std::string logfile = glogfile) : _type(type), _logfile(logfile)
            {
            }
            
            // 启用指定输出类型
            void Enable(int type)
            {
                _type = type;
            }
            
            // 记录日志的主方法
            void Logmessage(int grade, std::string filename, int filenumber, const char *format...)
            {
                LockGuard(); // 加锁保证线程安全
                
                logmessage log_message;
                
                // 填充日志消息结构
                log_message._grade = grade;
                log_message._filename = filename;
                log_message._filenumber = filenumber;
                log_message._pid = getpid(); // 获取当前进程ID
    
                // 处理可变参数
                va_list ap;
                va_start(ap, format);
                char log_info[1024];
                vsnprintf(log_info, sizeof(log_info), format, ap); // 格式化消息
                log_message._message = log_info;
                va_end(ap);
    
                log_message._current_time = GetCurrentTime(); // 获取当前时间
    
                ShowLog(&log_message); // 显示日志
            }
    
        private:
            int _type;              // 输出类型
            std::string _logfile;   // 日志文件路径
        };
  • 接口层 (宏定义):提供简洁的用户接口,隐藏底层复杂性,让使用者只需关心核心信息即可。

cpp 复制代码
   // 全局日志对象
    Log lg;
    
    // 启用屏幕输出的宏
#define EnableScreen()          \
    do                          \
    {                           \
        lg.Enable(SCREEN_TYPE); \
    } while (0)
    
    // 启用文件输出的宏  
#define EnableFile()          \
    do                        \
    {                         \
        lg.Enable(FILE_TYPE); \
    } while (0)
    
    // 主要的日志记录宏
#define LOG(Gread, Format, ...)                                          \
    do                                                                   \
    {                                                                    \
        lg.Logmessage(Gread, __FILE__, __LINE__, Format, ##__VA_ARGS__); \
    } while (0)

通过宏定义提供简洁的接口,可以做到:

  • 编译时优化:编译器可以内联展开,减少函数调用开销
  • 自动上下文 :自动填充__FILE____LINE__等编译时常量
  • 代码简洁:用户只需关注核心信息,无需重复输入样板代码

我们在后文线程池中使用日志系统,所以暂未给出使用示例,请关注后文。

线程池设计实现

线程池是提前创建一批线程,等到我们有任务的需要被执行的时候就可以传递给线程池,让其其中提前创建的线程帮我们去处理任务,符合生产消费模型。

我们选择使用vector来管理这批线程,线程由我们手动进行封装,使其符合面向对象的设计思路,封装代码可以参考文章Linux线程控制(点击即可跳转)中的线程封装:用vector管理一批线程对象就可以管理一批线程,同时加入任务队列,让这批线程在队列里不断获取任务然后去执行。

我们需要在线程池的类中设计一个函数HandlerTask,作为线程类中回调函数 ,但是线程封装遇到的问题相同,他有隐含的this指针,不符合线程创建所需的指针类型,因此我们需要使用bind来绑定成员函数。

using func_t = std::function<void *(const std::string &)>;

func_t func = std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1);

这样就可以将含有this指针与HandlerTask强行绑定,使得func就是我们所需要的类型,但同时又可以调用到HandlerTask函数。将func函数对象传入即可

HandlerTask内部则需像正常的消费者一样,从任务队列里获取任务,并且执行即可,但是要进行加锁保证线程安全

代码示例:

ThreadPool.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <queue>
#include <vector>
#include <pthread.h>
#include <functional>
#include "Thread.hpp"
#include <string>
#include <unistd.h>
#include "Task.hpp"
#include "Log.hpp"
#include "LockGuard.hpp"

const int gdefalut = 5;  // 默认线程池大小
using namespace log_ns;

template <class T>
class ThreadPool
{
    typedef MyThread::Thread Thread;
    using func_t = std::function<void *(const std::string &)>;

private:
    // 互斥锁操作封装
    void Lock()
    {
        pthread_mutex_lock(&_task_queue_mutex);
    }
    void UnLock()
    {
        pthread_mutex_unlock(&_task_queue_mutex);
    }
    
    // 条件变量操作封装
    void CondWait()
    {
        pthread_cond_wait(&_task_queue_cond, &_task_queue_mutex);
    }
    void WakeUp()
    {
        pthread_cond_signal(&_task_queue_cond);
    }
    void WakeUpAll()
    {
        pthread_cond_broadcast(&_task_queue_cond);
    }

    // 判断任务队列是否为空
    bool IsEmpty()
    {
        return _task_queue.empty();
    }
    
    void *HandlerTask(const std::string &name)
    {
        while (true)
        {
            Lock();
            // 使用while防止虚假唤醒
            while (IsEmpty() && _isrunning)
            {
                _sleep_thread_num++;
                LOG(INFO, "%s sleep begin\n", name.c_str());
                CondWait();  // 等待条件变量,释放锁并进入休眠
                _sleep_thread_num--;
                LOG(INFO, "%s wakeup\n", name.c_str());
            }
            
            // 检查线程池是否停止且队列为空,是则退出
            if (IsEmpty() && !_isrunning)
            {
                UnLock();
                break;
            }

            // 从任务队列中取出任务
            T &task = _task_queue.front();
            LOG(DEBUG, "%s get a task : %s\n", name.c_str(), task.Debug().c_str());
            _task_queue.pop();
            UnLock();
            
            // 执行任务(在锁外执行,提高并发性)
            task();
            LOG(DEBUG, "%s handler a task : %s\n", name.c_str(), task.Result().c_str());
        }
        return nullptr;
    }
    
 
   // 初始化线程池中的工作线程
    void init()
    {
        // 使用bind绑定成员函数,创建线程执行函数
        func_t func = std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1);
        _threads.reserve(_max_cap);
        
        // 创建指定数量的工作线程
        for (int i = 0; i < _max_cap; i++)
        {
            std::string name = "thread-" + std::to_string(i + 1);
            _threads.emplace_back(name, func);  // 原地构造线程对象
        }
    }

    public: //构造函数(如果是单例模式,则需设为私有,只需将public注释即可)
    ThreadPool(int max_cap = gdefalut) : _max_cap(max_cap), _isrunning(false) //max_cap 线程池最大容量
    {
        LOG(INFO, "ThreadPool creat\n");

        // 初始化同步原语
        pthread_mutex_init(&_task_queue_mutex, nullptr);
        pthread_cond_init(&_task_queue_cond, nullptr);
        
        // 初始化线程和启动线程池
        init();
        start();
    }

  		// 启动线程池中的所有工作线程
    void start()
    {
        LOG(INFO, "ThreadPool start!\n");

        _isrunning = true;
        for (auto &thread : _threads)
        {
            thread.start();  // 启动每个工作线程
        }
    }

public:

     // 向线程池添加任务(生产者接口)
     // task 要执行的任务
 
    void Equeue(T &task)
    {
        Lock();
        if (_isrunning)
        {
            _task_queue.push(task);  // 将任务加入队列

            // 如果有休眠的线程,唤醒其中一个
            if (_sleep_thread_num > 0)
                WakeUp();
        }
        UnLock();
    }

     // 停止线程池
     // 设置运行标志为false,并唤醒所有休眠线程让其自然退出
  
    void stop()
    {
        Lock();
        _isrunning = false;

        WakeUpAll();  // 唤醒所有休眠线程,让它们检查退出条件
        LOG(INFO, "ThreadPool stop!\n");

        UnLock();
    }

    // 析构函数
    ~ThreadPool()
    {
        // 销毁同步原语
        pthread_mutex_destroy(&_task_queue_mutex);
        pthread_cond_destroy(&_task_queue_cond);
        LOG(INFO, "ThreadPool destroy\n");
    }

    // 单例模式相关方法
    static ThreadPool<T> *getInstance()
    {
        if (_tp == nullptr)//使用双重检查锁定实现线程安全的单例
        {
            LockGuard(_tp_mutex);  // 加锁保证线程安全
            if (_tp == nullptr)    // 再次检查(双重检查锁定)
            {
                _tp = new ThreadPool<T>;
                LOG(INFO, "the signal Threadpool is creater\n");
            }
            else
            {
                return _tp;
            }
        }
        return _tp;
    }

private:
    // 线程池成员变量
    int _max_cap;                   // 线程池最大容量
    std::queue<T> _task_queue;      // 任务队列
    std::vector<Thread> _threads;   // 工作线程集合
    bool _isrunning;                // 线程池运行状态标志
    int _sleep_thread_num;          // 当前休眠的线程数量

    // 同步原语
    pthread_mutex_t _task_queue_mutex;  // 保护任务队列的互斥锁
    pthread_cond_t _task_queue_cond;    // 任务队列条件变量

    // 单例模式相关静态成员
    //static ThreadPool<T> *_tp;       // 单例指针
    //static pthread_mutex_t _tp_mutex; // 保护单例创建的互斥锁
};

// 以下代码为单例模式的成员初始化
// 静态成员初始化
//template <class T>
//ThreadPool<T> *ThreadPool<T>::_tp = nullptr;

//template <class T>
//pthread_mutex_t ThreadPool<T>::_tp_mutex = PTHREAD_MUTEX_INITIALIZER;

Task.hpp(简单封装的任务类)

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


struct Task{
    Task()
    {

    }
    Task(int x, int y):_x(x),_y(y)
    {
    }
    void operator()()
    {
        _result = _x + _y;
    }
     std::string Debug() 
    {
        std::string ret = std::to_string(_x) + "+" + std::to_string(_y) + "=" + "?"; 
        return ret;
    }
    std::string Result() 
    {
        std::string ret = std::to_string(_x) + "+" + std::to_string(_y) + "=" + std::to_string(_result); 
        return ret;

    }


    int _x;
    int _y;
    int _result;
};

main.cpp

cpp 复制代码
#include "ThreadPool.hpp"
#include <iostream>
#include <unistd.h>
#include "Task.hpp"
#include "Log.hpp"
using namespace log_ns;
int main()
{
    srand(time(nullptr) ^ getpid());

    ThreadPool<Task> *threads = new ThreadPool<Task>;

    threads->init();
    threads->start();

    for (int i = 0; i < 5; i++)
    {
        Task task(rand() % 10, rand() % 10);
        threads->Equeue(task);
        sleep(1);
    }
    threads->stop();
    delete threads;

    return 0;
}

运行结果:(日志打印)

单例模式下的线程池

什么是单例模式?

在程序的整个运行过程中,某个类只能有一个实例对象存在。想象一下,如果你在一个公司里,财务部门只能有一个总监,无论谁要处理财务事务,都必须通过这个唯一的总监来进行,这就是单例模式的现实写照。它确保了一个类的全局唯一性,避免了重复创建带来的资源浪费和状态不一致问题。

为什么需要这种限制?

在实际的软件开发中,有些对象确实不应该被重复创建。比如数据库连接池,如果每个模块都创建自己的连接池,不仅浪费资源,还可能导致数据库连接数超限。再比如配置管理器,如果存在多个配置实例,就可能出现配置信息不一致的混乱局面。单例模式正是为了解决这类问题而生的,它通过技术手段强制保证了实例的唯一性。

饿汉式单例:提前准备的稳妥方案

饿汉式单例的思路很直接------在程序启动之初,就立即创建好这个唯一的实例。这种做法很像一个人吃饭,吃完饭就把碗洗了,确保下次吃饭可以直接使用碗

它的实现原理是在类加载阶段就完成实例的初始化,这个时机远远早于任何线程的访问。由于实例的创建发生在所有线程启动之前,自然就不存在线程安全的问题。这种方式的优点很明显:实现简单,不需要考虑复杂的同步机制,获取实例时直接返回即可,性能很好。

但饿汉式也有明显的缺点。如果这个实例创建成本很高,但程序运行过程中可能根本用不到它,那就造成了资源浪费。就像提前准备了一桌丰盛的宴席,但客人可能根本不来吃饭。

懒汉式单例:按需创建的聪明做法

与饿汉式的"提前准备"不同,懒汉式采用了"按需创建"的策略。它不会在程序启动时就创建实例,而是等到第一次有人真正需要这个实例时才进行创建。就像一个人吃完饭不洗碗,等到下次吃饭时需要用碗的时候在洗碗。

这种延迟初始化的思路很聪明,它避免了不必要的资源占用。只有在确实需要的时候才付出创建成本,这在很多场景下都是更合理的选择。但懒汉式也带来了新的挑战------线程安全问题。

想象一下,在多个线程同时首次请求实例的情况下,如果不加控制,每个线程都可能检测到实例不存在,然后各自创建一个实例,这就违背了单例的初衷。

上述代码取消掉注释的代码,就是懒汉方式实现的单例模式,其中注释部分为单例模式所需添加的代码

线程安全的实现方式

为了解决懒汉式的线程安全问题,开发者们想出了多种方案。最简单的做法是每次获取实例时都加锁,但这样性能代价太高。后来出现了双重检查锁定这种更精巧的方案------先不加锁检查实例是否存在,如果不存在再进入同步块,进入后再次检查实例是否存在,这样既保证了线程安全,又避免了不必要的锁竞争。

cpp 复制代码
  static ThreadPool<T> *getInstance()
    {
        if (_tp == nullptr)//使用双重检查锁定实现线程安全的单例
        {
            LockGuard(_tp_mutex);  // 加锁保证线程安全
            if (_tp == nullptr)    // 再次检查(双重检查锁定)
            {
                _tp = new ThreadPool<T>;
                LOG(INFO, "the signal Threadpool is creater\n");
            }
            else
            {
                return _tp;
            }
        }
        return _tp;
    }

在第一次申请的时候,_tp为空,此时若有多个线程同时申请,则会创建多份实例,因此就需要加锁,来保证只有一个线程创建出实例。

等到创建后,其他线程在去判断的时候 _tp已经不为空了,此时会直接返回已经创建的 _tp。此时已经创建成功了,后续还有线程需要使用该函数时,不需要创建实例,只需返回即可,不存在线程安全问题了,即线程安全问题只在第一批并发申请 _tp的线程间存在。

那么之后再去加锁就大大降低效率了,因此我们在加一层判断,后续进来的线程直接判断 _tp不为空直接返回即可,无需加锁,这种方式即为双重检查锁定。

在现代C++中,还可以利用语言特性,通过局部静态变量来实现既线程安全又简洁的懒汉式单例,这通常被认为是最优雅的实现方式。

单例模式使用示例:

cpp 复制代码
#include"ThreadPool.hpp"
#include<iostream>
#include<unistd.h>
#include"Task.hpp"
#include"Log.hpp"
using namespace log_ns;
int main()
{
    srand(time(nullptr) ^ getpid());
   for(int i = 0;i<5;i++)
    {
        Task task(rand()%10,rand()%10);
        ThreadPool<Task>::getInstance()->Equeue(task); //任何访问线程池的操作都通过getInstance()方法实现
        sleep(1);
    }
    ThreadPool<Task>::getInstance()->stop(); 
    delete ThreadPool<Task>::getInstance();
    return 0;
}

由于单例子模式在创建阶段结要启动线程,因此不需要我们手动init,start,我们需要将这些方法都设为private,只暴露getInstance(),Equeue(),stop(),以及析构函数即可。

相关推荐
CV搬运专家5 小时前
Rust 控制流深度解析:安全保证与迭代器哲学
java·开发语言
lkx097885 小时前
笔记C++语言,太焦虑了
前端·c++·笔记
云边有个稻草人5 小时前
深入解析 Rust 内部可变性模式:安全与灵活的完美平衡
开发语言·安全·rust
张泽腾665 小时前
<FreeRTOS>
java·开发语言
云边有个稻草人5 小时前
所有权与解构(Destructuring)的关系:Rust 中数据拆分的安全范式
开发语言·安全·rust
Gold Steps.6 小时前
常见的Linux发行版升级openSSH10.+
linux·运维·服务器·安全·ssh
laocooon5238578866 小时前
一个蛇形填充n×n矩阵的算法
数据结构·算法
绛洞花主敏明6 小时前
Go语言中json.RawMessage
开发语言·golang·json
hello_2506 小时前
golang程序对接prometheus
开发语言·golang·prometheus