OS81.【Linux】基于环形队列的多生产者-多消费者模型

目录

[1.知识回顾: 基于环形队列的单生产者-单消费者模型](#1.知识回顾: 基于环形队列的单生产者-单消费者模型)

2.改为多生产者-多消费者版本

ring_queue类

构造函数

析构函数

lock

unlock

push

加锁放在P操作前还是P操作后?

解锁放在V操作前函数V操作后?

pop

main函数

3.拓展阅读


1.知识回顾: 基于环形队列的单生产者-单消费者模型

参见OS81.【Linux】基于环形队列的单生产者-单消费者模型文章,只用了信号量,来保证生产者和消费者之间的互斥关系

2.改为多生产者-多消费者版本

生产者线程函数:

cpp 复制代码
void* produce(void* args)
{
    ring_queue<int>* bq=static_cast<ring_queue<int>*>(args);
    for (;;)
    {
        sleep(1);
        int data=rand()%10;
            bq->push(data);
        printf("生产者生产了数据%d\n",data);
    }
    return nullptr;
}

消费者线程函数:

cpp 复制代码
void* consume(void* args)
{
    ring_queue<int>* bq=static_cast<ring_queue<int>*>(args);
    for (;;)
    {
        sleep(3);
        int data=bq->pop();
        printf("消费者消费了数据%d\n",data);
    }
    return nullptr;
}

ring_queue内:

cpp 复制代码
void push(const T& obj)
{
    P(_p_space_sem);
    _ring_queue[_p_i]=obj;
    _p_i++;
    _p_i%=_max_capacity;
    V(_c_data_sem);
}
T pop()
{
    P(_c_data_sem);
    T ret=_ring_queue[_c_i];
    _c_i++;
    _c_i%=_max_capacity;
    V(_p_space_sem);
    return ret;
}

如果改成多生产者-多消费者版本,多个生产者可能同时执行P(_p_space_sem)操作,这样多个生产者访问的是环形队列的同一个元素,因为只有一个_p_i,同理,多个消费者也可能同时执行P(_c_data_sem)操作,这样多个消费者访问的也是环形队列的同一个元素,因为也只有一个_c_i

为了防止数据不一致的问题,生产者之间必须保证互斥,同样消费者之间也必须保证互斥,可以加锁来实现,而且是加2把锁,一个保证生产者之间是互斥的,一个保证消费者之间是互斥的

cpp 复制代码
//为class ring_queue添加2个成员变量
pthread_mutex_t _c_lock; //保证消费者之间是互斥的
pthread_mutex_t _p_lock; //保证生产者之间是互斥的

结论: 任何一个时刻,只能有一个消费者(为满)或一个生产者(为空)或一个消费者和一个生产者(不空不满)访问环形队列

ring_queue类

构造函数

添加锁的初始化,默认属性

cpp 复制代码
ring_queue(size_t max_capacity=20)
:_max_capacity(max_capacity)
,_c_i(0)
,_p_i(0)
{
    sem_init(&_c_data_sem,0,0);
    sem_init(&_p_space_sem,0,_max_capacity);
    pthread_mutex_init(&_c_lock);
    pthread_mutex_init(&_p_lock);
    _ring_queue.resize(_max_capacity);
}

析构函数

添加锁的销毁

cpp 复制代码
~ring_queue()
{
    sem_destroy(&_c_data_sem);
    sem_destroy(&_p_space_sem);
    pthread_mutex_destroy(&_c_lock);
    pthread_mutex_destroy(&_p_lock);
    _ring_queue.clear();
}

lock

封装了加锁的步骤

cpp 复制代码
//private内
void lock(pthread_mutex_t& lock)
{
    pthread_mutex_lock(&lock);
}

unlock

封装了解锁的步骤

cpp 复制代码
//private内
void unlock(pthread_mutex_t& lock)
{
    pthread_mutex_unlock(&lock);
}

push

加锁放在P操作前还是P操作后?

方案1:

cpp 复制代码
void push(const T& obj)
{
    lock(_p_lock);
    P(_p_space_sem);
    //......
}

方案2:

cpp 复制代码
void push(const T& obj)
{
    P(_p_space_sem);
    lock(_p_lock);
    //......
}

换句话说,方案1和方案2哪个可以?

答:需要明确2点:

  1. 加锁和解锁的目的是保护生产者下标_p_i和消费者的下标_c_i,因此访问_p_i和_c_i之前需要加锁,访问完后就解锁,从这里看,两个方案都行

2.申请信号量是原子的,也就是说没有必要加锁保护信号量,选方案2

方案2比方案1更高效,如果选方案1,先申请锁后申请信号量,那么先申请锁和后申请信号量串行的,效率低

方案2好在多个线程可以同时占位置,即多个线程可以同时申请信号量(多线程先申请信号量,等待其中一个线程释放锁,其它线程可以立即获得锁)可以做到一定程度上并行,效率高

举个方案2的例子,现在环形队列有5个空位,3个生产者线程都来生产,它们申请信号量必定成功,因为5>3,但是只能有一个线程访问队列写入数据,因为由锁管控

也可以从临界区看,临界区越小越好,执行效率越高,通俗解释: 先预订后排队比先排队后预订效率高

总而言之: 方案1和方案2都可以,但是方案2效率更高,并发度好

解锁放在V操作前函数V操作后?

根据前面的描述,这样写就行:

cpp 复制代码
void push(const T& obj)
{
    P(_p_space_sem);
    lock(_p_lock);
    _ring_queue[_p_i]=obj;
    _p_i++;
    _p_i%=_max_capacity;
    unlock(_p_lock);
    V(_c_data_sem);
}

pop

cpp 复制代码
T pop()
{
    P(_c_data_sem);
    lock(_c_lock);
    T ret=_ring_queue[_c_i];
    _c_i++;
    _c_i%=_max_capacity;
    unlock(_c_lock);
    V(_p_space_sem);
    return ret;
}

main函数

改成多生产者、多消费者

cpp 复制代码
int main()
{
    pthread_t c_tid[NUM];
    pthread_t p_tid[NUM];
    srand((unsigned int)time(NULL));
    ring_queue<int>* bq=new ring_queue<int>();
    
    for (int i=0;i<NUM;i++)
    {
        pthread_t producer,consumer;
        pthread_create(&producer,nullptr,produce,bq);
        pthread_create(&consumer,nullptr,consume,bq);
        c_tid[i]=consumer;
        p_tid[i]=producer;
    }
    for (int i=0;i<NUM;i++)
    {
        pthread_join(p_tid[i],nullptr);
        pthread_join(c_tid[i],nullptr);
    }
    delete bq;
    return 0;
}

运行结果:

生产者、消费者各sleep(1)

3.拓展阅读

本文实现了一个简单的环形队列,有兴趣的读者可以看看Linux内核的kfifo无锁环形队列,代码在/include/linux/kfifo.h和/lib/kfifo.c,显然内核将kfifo的声明与实现分离了

相关推荐
Dachui_11221 小时前
ZeroNews LFS 使用教程:NAS 文件发布为公网 HTTPS 文件空间,不传网盘、按人授权、免登录分享
运维·网络安全·文件共享·团队协作·nas
智能运维指南1 小时前
2026年企业自动化运维平台选型:四类架构的差异与决策逻辑
运维·人工智能·嘉为蓝鲸
探索云原生1 小时前
KubeClipper 1.7.0 发布:Operation 优化与 Kubernetes 1.37 支持
linux·docker·云原生·kubernetes·go
szephyr1 小时前
Nginx 反向代理实战:从 502、跨域到 HTTPS,一次打通
运维·nginx·https·部署·反向代理
一技安身2 小时前
【信创】docker-26.0.2.tgz 原理 + 完整安装
linux·运维·服务器
程曦曦2 小时前
MySQL 生产库误删 98 张表后的时间点恢复实战:从 binlog 解析到资金对账
linux·数据结构·其他·算法·ubuntu·运维开发
Dachui_11222 小时前
内网穿透 Webhook 告警实战:客户端离线、应用探测、资源删除全覆盖
运维·远程工作·内网穿透·devops·webhook·服务器监控·应用探测
郝学胜-神的一滴2 小时前
Effective Python 条款 13:善用星号解包,告别下标切片拆分的坑
服务器·开发语言·数据结构·python·程序人生·pycharm
数据智研2 小时前
【数据分享】300个城市-5G试点城市DID数据(2014-2025)
大数据·运维·人工智能·信息可视化·数据分析
honsor2 小时前
工业级网口温湿度变送器 ModbusTCP 机房动环环境监测终端
运维·网络·人工智能·物联网·安全·云计算·智能温湿度监测系统