目录
[1.知识回顾: 基于环形队列的单生产者-单消费者模型](#1.知识回顾: 基于环形队列的单生产者-单消费者模型)
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点:
- 加锁和解锁的目的是保护生产者下标_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的声明与实现分离了