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

相关推荐
Felven10 小时前
stress-ng 与 fio 性能测试工具使用指南
linux·测试工具·fio·stress-ng
XiaoMaqqqq10 小时前
市面上正规的IP驱动产业新场景新工具哪家强
运维·python·网络协议·tcp/ip
huainingning11 小时前
RJ SW Console口忘记密码处理方法
linux·运维·服务器
布裘11 小时前
【银河麒麟】桌面系统循环登录,无法进入桌面?
linux·运维·服务器
慧都小项11 小时前
程序到了Linux才出错?用CLion把调试接到目标环境
linux·运维·服务器
Lsetea12 小时前
OpenSSL verify报unhandled critical extension:error 34与关键扩展排查
运维·https·ssl证书·openssl·证书链
guo_wen_qiang12 小时前
docker的备份与迁移
运维·docker·容器
Ruiery13 小时前
Linux 6.6内核内存管理深度解析(一):物理内存初始化 — memblock 怎么把内存交给 buddy
linux·运维·服务器
2401_8685347813 小时前
无线网络规划设计
服务器·网络
库拉镜像AI牛牛14 小时前
短剧内容自动化生产:知漫剧工作室落地教程
大数据·服务器·前端·人工智能·语音识别