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的声明与实现分离了

相关推荐
U-Mail邮件系统1 小时前
企业邮箱搭建用自建还是租用?合规、成本与安全性对比解析
服务器·网络·企业邮箱系统
X1A0RAN1 小时前
Jenkins Pipeline 变量打印指南
运维·servlet·jenkins
吴爃1 小时前
小微企业 SRE 稳定性建设(二):上线前 P0 验收项目
运维·可用性测试·稳定性·故障
llilian_161 小时前
基于PCI总线校时时钟同步卡的高精准授时解决方案
大数据·服务器·功能测试·单片机
运维大师2 小时前
【K8S 运维实战】37-金融企业多集群落地
运维·金融·kubernetes
米修米修ne2 小时前
Home Assistant连接米家MIoT和苹果HomeKit折腾笔记
服务器·智能家居
行者-全栈开发2 小时前
【Linux内核】CVE-2026-46242:Bad Epoll Linux 内核 epoll UAF 漏洞修复指南(99%可靠root的云安全噩梦)
linux·运维·服务器·cve-2026-46242·linux内核漏洞·use-after-free·云多租户安全
平安的平安2 小时前
人在外地,怎样访问办公室里的电脑和内部资源?
运维·服务器·电脑
智体工坊2 小时前
从零搭建 AI 模型中转网关:部署、渠道、定价、装修全记录
运维·服务器·人工智能·搜索引擎·自动化