《从内核视角看 Linux:环形缓冲区 + 线程池的生产消费模型实现》

**前引:**环形缓冲区的低开销数据传输、线程池的灵活资源调度,让二者的结合成为 Linux 高性能并发的 "黄金搭档"。无论是服务器开发、嵌入式编程还是底层工具构建,掌握环形生产消费模型与线程池的设计逻辑,都能帮你突破并发性能瓶颈。接下来,我们从基础到进阶,逐步解锁这两项核心技术!

目录

【一】环形生产消费模型介绍

【二】信号量使用

(1)创建信号量对象

(2)初始化信号量

(3)信号量等待

(4)信号量释放

(5)信号量销毁

(6)获取当前信号量

【三】模型实现

(1)理论框架

(2)生产实现

(3)消费实现

【四】线程池

(1)理论框架

(2)生产实现

(3)消费实现

(4)任务执行

(5)main函数

(6)效果展示

【五】单例模式

(1)什么是单例模式

(2)如何完成单例模式

(3)例如:


【一】环形生产消费模型介绍

"环形"生产消费模型:队列采⽤数组模拟,⽤模运算来模拟环状特性,例如:

特点:

(1)缓存大小初始后是固定的

(2)也符合数据"先进先出"的特点

(3)通过首尾下标来访问数据。Head:只要有空位置就可以一直存放数据

Tail:只要有数据就可以一直获取数据

而位置是否有数据我们可以根据信号量(引用计数,本质为临界资源数量)判断!

(此模型较于普通的"生产消费模型"可以根据信号量做到并发执行,信号量代表资源数量)

为什么信号量不用担心两个线程同时获取一个信号量?

核心原因是sem_wait操作的原子性------ 它将 "检查信号量值" 和 "修改信号量值" 封装为一个不可分割的步骤,确保任何时刻只有一个线程能成功获取资源

**例如:**核心思想就是"连续",不给其它线程可乘之机

原子操作的关键是 "没有中间状态":

  • 信号量值要么是 1(没被拿),要么是 0(被拿了),不会出现 "0.5" 这种中间值;
  • 线程要么拿到资源(信号量值变 0),要么没拿到(阻塞),不会出现 "两个线程都认为自己拿到了" 的情况。

这就是为什么哪怕两个线程 "同时" 抢最后一个信号量,也绝对不会同时拿到 ------ 原子性把 "争夺" 变成了 "排队",先到先得

【二】信号量使用

(1)创建信号量对象

原型:

cpp 复制代码
sem_t 对象变量名;
(2)初始化信号量

原型:

cpp 复制代码
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

参数:

sem:指向信号量对象的指针(需提前声明,如sem_t empty_sem;

pshared:0 表示信号量用于线程间同步(环形模型必选);非 0 表示用于进程间同步(不常用)

value:信号量初始值(表示可用资源的初始数量)

作用:初始化一个信号量

(3)信号量等待

原型:

cpp 复制代码
int sem_wait(sem_t *sem);

参数:指向目标信号量对象的指针

作用:尝试获取信号量资源(将信号量值减 1)

若当前信号量值为 0,线程会阻塞等待,直到有其他线程释放资源(信号量值 > 0)

(4)信号量释放

原型:

cpp 复制代码
int sem_post(sem_t *sem);

参数:指向目标信号量对象的指针

作用:释放信号量资源(将信号量值加 1),并唤醒一个阻塞在该信号量上的线程(若有)

(5)信号量销毁

原型:

cpp 复制代码
int sem_destroy(sem_t *sem);

参数:指向目标信号量对象的指针

作用:销毁已初始化的无名信号量,释放其占用的资源

(6)获取当前信号量

原型:

cpp 复制代码
int sem_getvalue(sem_t *sem, int *sval);

参数:获取信号量的当前值

**第一个参数:**要获取的信号量对象指针

**第二个参数:**用于存储信号量当前值

作用:

【三】模型实现

(1)理论框架

生产和消费是根据信号量来判断的,可以同步,所以需要两个信号量初始化

(生产:初始资源为MAX)

(消费:初始资源为0)

在单生产消费的基础上,同样只能单个线程生产与消费,但是可以并发执行,所以需要两个互斥锁

所以类的设计:

cpp 复制代码
//最大容量
#define MAX 5

template<class T>
class cycle_p_c
{
public:
    cycle_p_c()
    {
        //开缓存空间
        _buffer.resize(MAX);
        //锁
        pthread_mutex_init(&p_mutex,NULL);
        pthread_mutex_init(&c_mutex,NULL);
        //信号量
        sem_init(&p_message,0,MAX);
        sem_init(&c_message,0,0);
        //下标
        head,tail=0;
    }
    //生产
    void push_back(const T& date)
    {

    }
    //消费
    void pop()
    {
        
    }
private:
    //数据缓存
    std::vector<T> _buffer;
    //信号变量*2
    sem_t p_message;
    sem_t c_message;
    //两个下标
    size_t head;
    size_t tail;
    //两个互斥锁(确保每次只有一个生产和一个消费并发执行)
    pthread_mutex_t p_mutex;
    pthread_mutex_t c_mutex;
};
(2)生产实现

先判断信号量,如果生产的信号量充足(原子性),就可以直接去进入生产(否则就自动等待):

生产需要保证互斥性,否则可能导致超出资源范围,根据模运算来获取资源下标

cpp 复制代码
//生产
    void push_back(const T& date)
    {
        //看信号量是否足够(生产信号量-1)
        sem_wait(&p_message);
        //此时说明已经申请到信号量
        pthread_mutex_lock(&p_mutex);
        
        //生产
        head%=MAX;
        _buffer[head++]=date;
        //消费信号量+1
        sem_post(&c_message);

        std::cout<<"我生产了:"<<date<<std::endl;
        
        pthread_mutex_unlock(&p_mutex);
    }
(3)消费实现

先判断信号量,如果消费的信号量不为0(原子性),就可以直接去进入消费(否则就自动等待)

消费需要保证互斥性,否则可能导致超出资源范围,根据模运算来获取资源下标

cpp 复制代码
//消费
    void pop()
    {
        //看信号量是否足够(消费信号量-1)
        sem_wait(&c_message);
        //此时说明已经申请到信号量
        pthread_mutex_lock(&c_mutex);
        
        //消费
        std::cout<<"我消费了:"<<_buffer[tail]<<"...."<<std::endl;
        _buffer[tail++]=0;
        //生产信号量+1
        sem_post(&p_message);
        
        pthread_mutex_unlock(&c_mutex);
    }

【四】线程池

线程池我们采用上面的"生产消费模型"或者"环形生产消费模型"来实现都可以,这里以后者为例:

我的设计思路:

主要还是环形生产消费模型的实现,其次在pop()的时候只需要将任务拿出来交给执行函数就可以了,因为是一个线程一次pop(),所以执行任务是不需要加锁的,当然如果你需要打印的话就需要加锁,且所有打印应该是同一把锁,否则打印无法准确观察任务的执行

(1)理论框架
cpp 复制代码
//最大容量
#define MAX 6
//随机数范围
constexpr int min_t=1;
constexpr int max_t=100;

struct Date
{
    Date() = default;
    Date(int date1,int date2)
    :_date1(date1),_date2(date2)
    {}

    int _date1;
    int _date2;
};

//获取随机数
int data_t() 
{
    // 1. 使用 std::thread_local 声明一个线程局部的 Mersenne Twister 生成器
    //    std::mt19937 是一个高质量的伪随机数生成器
    thread_local static std::mt19937 generator;
    
    // 2. 懒初始化:检查生成器是否已经被种子化
    //    (一个未被种子化的 std::mt19937 会产生固定的默认序列)
    thread_local static bool is_initialized = false;
    if (!is_initialized) {
        // 3. 使用 std::random_device 来获取一个真随机种子
        std::random_device rd;
        
        // 为了确保万无一失,我们可以将多个种子源组合起来
        // 这里我们结合了真随机数和线程ID的哈希值
        std::seed_seq seq{
            rd(), 
            static_cast<unsigned int>(std::hash<std::thread::id>()(std::this_thread::get_id()))
        };
        
        // 4. 用组合后的种子序列来初始化当前线程的生成器
        generator.seed(seq);
        
        is_initialized = true;
    }
    
    // 5. 定义一个分布,用于生成 [min_t, max_t] 范围内的整数
    std::uniform_int_distribution<int> distribution(min_t, max_t);
    
    // 6. 使用生成器和分布来产生最终的随机数
    return distribution(generator);
}

//任务执行方法
int calculate(struct Date date)
{
    return date._date1+date._date2;
}


//线程池
template<class T>
class cycle_p_c
{
public:
    cycle_p_c()
    {
        //缓存空间
        _buffer.resize(MAX);
        //锁
        pthread_mutex_init(&p_mutex,NULL);
        pthread_mutex_init(&c_mutex,NULL);
        pthread_mutex_init(&tasks,NULL);
        //信号量
        sem_init(&p_message,0,MAX);
        sem_init(&c_message,0,0);
        //下标
        tail=0;
        head=0;
    }
    ~cycle_p_c()
    {
        pthread_mutex_destroy(&p_mutex);
        pthread_mutex_destroy(&c_mutex);
        pthread_mutex_destroy(&tasks);
        sem_destroy(&p_message);
        sem_destroy(&c_message);
    }


    //生产
    void push_back(const T& date)
    {
       
    }
    //消费
    T pop()
    {
        
    }
    //处理任务
    void Handle_tasks(struct Date* ptr)
    {
        
    }
private:
    //数据缓存
    std::vector<T> _buffer;
    //信号变量*3
    sem_t p_message;
    sem_t c_message;
    //两个下标
    size_t head;
    size_t tail;
    //两个互斥锁(确保每次只有一个生产和一个消费并发执行)
    pthread_mutex_t p_mutex;
    pthread_mutex_t c_mutex;
    //任务拿取锁
    pthread_mutex_t tasks;
};
(2)生产实现

先看生产信号量(原子性,不需要加锁)是否充足,然后开启互斥锁,开始生产,给消费信号

cpp 复制代码
 //生产
    void push_back(const T& date)
    {
        //看信号量是否足够(生产信号量-1)
        sem_wait(&p_message);
        //此时说明已经申请到信号量
        pthread_mutex_lock(&p_mutex);
        
        //生产
        head%=MAX;
        _buffer[head++]=date;
        //消费信号量+1
        sem_post(&c_message);
        std::cout<<"我创建任务了哦..."<<std::endl;
        
        pthread_mutex_unlock(&p_mutex);
    }
(3)消费实现

看消费的信号是否充(原子性),随后开启锁,开始消费,返回任务,用于后面的任务执行函数

cpp 复制代码
  //消费
    T pop()
    {
        //看信号量是否足够(消费信号量-1)
        sem_wait(&c_message);
        //此时说明已经申请到信号量
        pthread_mutex_lock(&c_mutex);
        
        //消费
        struct Date date=_buffer[tail++];
        tail%=MAX;
        //生产信号量+1
        sem_post(&p_message);
        
        pthread_mutex_unlock(&c_mutex);
        return date;
    }
(4)任务执行

如果需要打印任务来进行就需要加锁,确保每个任务的执行打印是独一无二的,否则会乱序

cpp 复制代码
//处理任务
    void Handle_tasks(struct Date* ptr)
    {
        pthread_mutex_lock(&tasks);

        std::cout<<"date1="<<(ptr->_date1)<<" date2="<<(ptr->_date2)<<" "<<"date1+date2==" 
        <<calculate(*ptr)<<std::endl;

        pthread_mutex_unlock(&tasks);
    }
(5)main函数
cpp 复制代码
#include"thread_pool.h"

typedef cycle_p_c<Date> cycle;

//派发任务
void* haddle_p(void* arg)
{
    cycle *ptr=(cycle*)arg;

    //创建任务
    int date1=data_t();
    int date2=data_t();
    struct Date date(date1,date2);
    sleep(1);

    ptr->push_back(date);

    return NULL;
}
//处理任务
void* haddle_c(void* arg)
{
    cycle *ptr=(cycle*)arg;

    //先获取任务
    struct Date date=ptr->pop();
    //执行任务
    ptr->Handle_tasks(&date);

    return NULL;
}

int main()
{

    cycle *ptr=new cycle;
    while(1)
    {
        //任务生成
        pthread_t pthread_p;
        pthread_create(&pthread_p,NULL,haddle_p,ptr);

        //任务执行线程
        pthread_t pthread_c;
        pthread_create(&pthread_c,NULL,haddle_c,ptr);

        sleep(1);
    }

    return 0;
}
(6)效果展示

如图出现"一次创建""两次打印任务"是因为从内核来看该线程的时间片到了:线程完成对信号量的操作,此时有了消费信号,但无法执行打印,而生产和消费是并发执行的,此时就轮到消费了,生产需要重新进入运行队列重新执行,因此将打印提前,在线程的时间片结束前执行打印即可解决!

【五】单例模式

(1)什么是单例模式

某些类,只应该具有⼀个对象(实例),就称之为单例。这点我们将构造私,通过静态方式即可解决!

(为什么需要静态的?静态成员在进程形成时就已经形成,而普通类需要main函数栈帧形成)

而静态方式可以选择直接实例化对象,也可以先只形成对象指针(用的时候再实例化)

很明显,选择后者更佳,二者虽都是实例化一个对象,但是后者可以一定时刻减少内存负担!

(2)如何完成单例模式

总结:构造私有(不能delete,否则会调用系统默认的)+delete拷贝构造与赋值+锁+静态成 员内部形成唯一的对象指针并返回

(3)例如:
cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

template<class T>
class Ceshi
{
public:

    // 禁止拷贝构造和赋值操作
    Ceshi(const Ceshi&) = delete;
    Ceshi& operator=(const Ceshi&) = delete;

    static Ceshi<T>* handle()
    {
        if (instance == nullptr)
        {
            pthread_mutex_lock(&mutex);
            if (instance == nullptr)
            {
                instance = new Ceshi<T>;
            }
            pthread_mutex_unlock(&mutex);
        }
        return instance;
    }

    void text()
    {
        std::cout<<"Hello World"<<std::endl;
    }

private:
    Ceshi()
    {}
    static Ceshi<T>* instance;
};

// 模板类静态成员变量类外初始化
template<class T>
Ceshi<T>* Ceshi<T>::instance = nullptr;
相关推荐
Cincoze-Johnny1 小时前
Linux系统-应用问题全面剖析Ⅳ:德承工控机MD-3000在Ubuntu操作系统下[TPM功能]设置教程
linux·运维·ubuntu
默|笙1 小时前
【Linux】进程(1)
linux·运维·服务器
某林2121 小时前
基于ROS2与EKF的四轮差速机器人里程计精度优化:解决建图漂移与重影问题
linux·stm32·嵌入式硬件·slam·智能小车
dragoooon341 小时前
[Linux网络基础——Lesson7.「传输层协议 UDP 与 TCP」]
linux·网络·udp
wanhengidc1 小时前
云手机的不足之处有哪些?
运维·服务器·科技·智能手机·云计算
红袜子i1 小时前
解决 Ubuntu 中 apt-get update 因架构配置混乱导致的更新失败问题
linux·ubuntu·架构
Xの哲學1 小时前
Linux 分段卸载技术深度剖析
linux·服务器·网络·架构·边缘计算
Charles Shan1 小时前
Mac上的linux虚拟机踩坑日记
linux·macos
绝顶少年1 小时前
阿里云服务器磁盘空间扩展实战:从39GB到200GB无损扩容指南
服务器·阿里云·云计算