生产者消费者模型

目录

一、生产者消费者模型的概念

二、生产者消费者模型的特点

三、生产者消费者模型优点

四、基于BlockingQueue的生产者消费者模型

4.1 基本认识

4.2 模拟实现

五、POSIX信号量

5.1 信号量概念

5.2 信号量函数

5.2.1 初始化信号量

5.2.2 销毁信号量

5.2.3 等待信号量

5.2.4 发布信号量

六、二元信号量模拟互斥功能

七、基于RingQueue的生产者消费者模型

7.1 空间资源与数据资源

7.2 资源的申请与释放

7.3 两个规则

7.4 代码实现

7.5 信号量维护环形队列的原理


一、生产者消费者模型的概念

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题

生产者和消费者彼此之间不直接通讯,而通过容器来通讯,所以生产者生产完数据之后不用等待消费者处理,直接将生产的数据放到这个容器当中;消费者也不用找生产者索要数据,而是直接从这个容器中取数据。容器就类似于一个缓冲区,平衡了生产者和消费者的处理能力,这个容器完成了生产者和消费者之间的解耦

二、生产者消费者模型的特点

  • 三种关系: 生产者和生产者(互斥关系)、消费者和消费者(互斥关系)、生产者和消费者(互斥关系、同步关系)
  • 两种角色: 生产者和消费者(通常由进程或线程承担)
  • 一个交易场所: 通常指的是内存中的一段缓冲区(可以自己通过某种方式组织)

生产者和生产者、消费者和消费者、生产者和消费者,它们之间为什么会存在互斥关系?

介于生产者和消费者之间的容器可能会被多个执行流同时访问,因此需要将该临界资源用互斥锁保护起来。所以所有生产者和消费者都会竞争式的申请锁,因此生产者和生产者、消费者和消费者、生产者和消费者之间都存在互斥关系

生产者和消费者之间为什么会存在同步关系?

若一直让生产者生产,那么当生产者生产的数据装满容器后,生产者再生产数据就会生产失败。
反之,让消费者一直消费,那么当容器当中的数据被消费完后,消费者再进行消费就会消费失败。
虽然这样不会造成任何数据不一致的问题,但是这样会引起另一方的饥饿问题,是非常低效的。应该让生产者和消费者访问该容器时具有一定的顺序性,比如让生产者先生产,然后再让消费者进行消费。

注意: 互斥关系保证的是数据的正确性,而同步关系是为了让多线程之间协同起来

三、生产者消费者模型优点

  • 解耦
  • 支持并发,提高效率
  • 支持忙闲不均

若在主函数中调用某一函数,那么必须等该函数体执行完后才继续执行主函数的后续代码,因此函数调用本质上是一种紧耦合。对应到生产者消费者模型中,函数传参实际上就是生产者生产的过程,而执行函数体实际上就是消费者消费的过程,但生产者只负责生产数据,消费者只负责消费数据,在消费者消费期间生产者可以同时进行生产,因此生产者消费者模型本质是一种松耦合

四、基于BlockingQueue的生产者消费者模型

4.1 基本认识

在多线程编程中,阻塞队列(Blocking Queue)是一种常用于实现生产者消费者模型的数据结构

生产者消费者模型_第1张图片

其与普通的队列的区别在于:

  • 当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中放入了元素
  • 当队列满时,往队列里存放元素的操作会被阻塞,直到有元素从队列中取出

阻塞队列最经典的应用场景:管道

4.2 模拟实现

下面以单生产者、单消费者为例进行讲解与实现

#include 
#include 
#include 

template 
class BlockQueue
{
public:
    BlockQueue(size_t capacity = 4) : _capacity(capacity)
    {
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_full,nullptr);
        pthread_cond_init(&_empty,nullptr);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_full);
        pthread_cond_destroy(&_empty);
    }

    void push(const T& data)
    {
        pthread_mutex_lock(&_mutex);
		while (IsFull()) {//不能进行生产,直到阻塞队列可以容纳新的数据
			pthread_cond_wait(&_full, &_mutex);
		}
		_queue.push(data);
        std::cout << "Producer: " << data << std::endl;
		pthread_mutex_unlock(&_mutex);
		pthread_cond_signal(&_empty); //唤醒在empty条件变量下等待的消费者线程
    }
    void pop(T& data)
    {
        pthread_mutex_lock(&_mutex);
		while (IsEmpty()) {//不能进行消费,直到阻塞队列有新的数据
			pthread_cond_wait(&_empty, &_mutex);
		}
		data = _queue.front();
		_queue.pop();
        std::cout << "Consumer: " << data << std::endl;
		pthread_mutex_unlock(&_mutex);
		pthread_cond_signal(&_full); //唤醒在full条件变量下等待的生产者线程
    }
    
private:
    bool IsFull() { return _queue.size() == _capacity; }
    bool IsEmpty() { return _queue.empty(); }

private:
    std::queue _queue;
    size_t _capacity;
    pthread_mutex_t _mutex;
    pthread_cond_t _full;
    pthread_cond_t _empty;
};

判断是否满足生产消费条件时不能用if,而应该用while:

pthread_cond_wait函数有可能调用失败,调用失败后该执行流就会继续往后执行。为了避免出现上述情况,就要让线程被唤醒后再次进行判断,确认是否真的满足生产消费条件,因此这里必须要用while进行判断

生产者消费者步调一致

#include 
#include "BlockQueue.hpp"

void* Producer(void* arg)
{
    BlockQueue* bq = (BlockQueue*)arg;
	
	while (true) { //生产者不断进行生产
		sleep(1);
		int data = rand() % 100 + 1;
		bq->push(data);
	}
}
void* Consumer(void* arg)
{
    int data = 0;
    BlockQueue* bq = (BlockQueue*)arg;
	while (true) { //消费者不断进行消费
		sleep(1);
		bq->pop(data);
	}
}

int main() 
{
    pthread_t producer,consumer;
    BlockQueue* bq = new BlockQueue;

    pthread_create(&producer,nullptr,Producer,(void*)bq);
    pthread_create(&consumer,nullptr,Consumer,(void*)bq);

    pthread_join(producer,nullptr);
    pthread_join(consumer,nullptr);
    delete bq;

    return 0;
}

由于代码中生产者是每隔一秒生产一个数据,而消费者是每隔一秒消费一个数据,因此运行代码后我们可以看到生产者和消费者的执行步调是一致的 

生产者消费者模型_第2张图片

生产者速度快,消费者速度慢

void* Producer(void* arg)
{
    BlockQueue* bq = (BlockQueue*)arg;
	
	while (true) { //生产者不断进行生产
		int data = rand() % 100 + 1;
		bq->push(data);
	}
}
void* Consumer(void* arg)
{
    int data = 0;
    BlockQueue* bq = (BlockQueue*)arg;
	while (true) { //消费者不断进行消费
		sleep(1);
		bq->pop(data);
	}
}

生产者消费者模型_第3张图片

此时由于生产者生产的很快,运行代码后一瞬间生产者就将阻塞队列装满。此时生产者想要再进行生产就只能在full条件变量下进行等待,直到消费者消费完一个数据后,生产者才会被唤醒进而继续进行生产,生产者生产完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了

生产者速度慢,消费者速度快

void* Producer(void* arg)
{
    BlockQueue* bq = (BlockQueue*)arg;
	
	while (true) { //生产者不断进行生产
        sleep(1);
		int data = rand() % 100 + 1;
		bq->push(data);
	}
}
void* Consumer(void* arg)
{
    int data = 0;
    BlockQueue* bq = (BlockQueue*)arg;
	while (true) { //消费者不断进行消费
		bq->pop(data);
	}
}

生产者消费者模型_第4张图片

虽然消费者消费的快,但开始时阻塞队列中是没有数据的,因此消费者只能在empty条件变量下等待,直到生产者生产完一个数据后,消费者才会被唤醒进而进行消费,消费者消费完这一个数据后又会进行等待,因此生产者和消费者的步调就是一致的

设置唤醒策略

可以设置一些策略。譬如,下面当阻塞队列当中存储的数据大于队列容量的一半时,再唤醒消费者线程进行消费;当阻塞队列当中存储的数据小于队列容器的一半时,再唤醒生产者线程进行生产

void push(const T &data)
{
    pthread_mutex_lock(&_mutex);
    while (IsFull()) { // 不能进行生产,直到阻塞队列可以容纳新的数据
        pthread_cond_wait(&_full, &_mutex);
    }
    _queue.push(data);
    std::cout << "Producer: " << data << std::endl;
    if (_queue.size() >= _capacity / 2) {
        pthread_cond_signal(&_empty); // 唤醒在empty条件变量下等待的消费者线程
    }
    pthread_mutex_unlock(&_mutex);
}
void pop(T &data)
{
    pthread_mutex_lock(&_mutex);
    while (IsEmpty()) { // 不能进行消费,直到阻塞队列有新的数据
        pthread_cond_wait(&_empty, &_mutex);
    }
    data = _queue.front();
    _queue.pop();
    std::cout << "Consumer: " << data << std::endl;
    if (_queue.size() <= _capacity / 2) {
        pthread_cond_signal(&_full); // 唤醒在full条件变量下等待的生产者线程
    }
    pthread_mutex_unlock(&_mutex);
}

生产者消费者模型_第5张图片

仍然让生产者生产快,消费者消费慢。运行代码后生产者还是一瞬间将阻塞队列装满后进行等待,但此时不是消费者消费一个数据就唤醒生产者线程,而是当阻塞队列当中的数据小于等于队列容器的一半时,才会唤醒生产者线程进行生产

基于任务的生产者消费者模型

实际使用生产者消费者模型时可不是简单的让生产者生产一个数字让消费者进行打印而已,前面的代码只是为了理解生产者消费者模型而已。
编写BlockingQueue时当中存储的数据就进行了模板化,那么就可以让BlockingQueue当中存储其他类型的数据。

譬如编写一个Task类(其中包含需要执行的任务),BlockingQueue中就存储Task对象。此时生产者放入阻塞队列的数据就是Task对象,而消费者从阻塞队列拿到Task对象后,就可以用该对象调用Run成员函数进行数据处理。

总之,根据需要进行编写即可

五、POSIX信号量

5.1 信号量概念

  • 可能会被多个执行流同时访问的资源被称为临界资源,临界资源需要进行保护否则会出现数据不一致等问题
  • 当仅用一个互斥锁对临界资源进行保护时,相当于将这块临界资源看作一个整体,同一时刻只允许一个执行流对这块临界资源进行访问(串行访问)
  • 实际上可以将这块临界资源再分割为多个区域,当多个执行流需要访问临界资源时,若这些执行流访问的是临界资源的不同区域,那么可以让这些执行流同时访问临界资源的不同区域,此时并不会出现数据不一致等问题

信号量本质是一个计数器,是描述临界资源中资源数目的计数器,信号量能够更细粒度的对临界资源进行管理

每个执行流在进入临界区之前都应先申请信号量,申请成功就有了操作临界资源的权限,当操作完毕后就应该释放信号量

生产者消费者模型_第6张图片

  • 信号量的P操作:将申请信号量称为P操作,申请信号量的本质就是申请获得临界资源中某块资源的使用权限,当申请成功时临界资源中资源的数目应该减一,因此P操作的本质就是让计数器减一
  • 信号量的V操作:释放信号量称为V操作,释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的数目就应该加一,因此V操作的本质就是让计数器加一

PV操作为原子操作

多个执行流为了访问临界资源会竞争式的申请信号量,因此信号量是会被多个执行流同时访问的,即信号量本质也是临界资源。但信号量本质就是用于保护临界资源的,所以信号量的PV操作必须是原子操作

注意: 内存当中变量的自增、自减操作并不是原子操作,因此信号量不可能只是简单的对一个全局变量进行自增、自减操作

申请信号量失败被挂起等待

当执行流在申请信号量时,可能此时信号量的值为0,即信号量描述的临界资源已全部被申请了,此时该执行流就应该在该信号量的等待队列中进行等待,直到有信号量被释放时被唤醒

注意: 信号量的本质是计数器,但不意味着只有计数器,信号量还包括一个等待队列

5.2 信号量函数

5.2.1 初始化信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);
  • sem:需要初始化的信号量
  • pshared:传入0值表示线程间共享,传入非零值表示进程间共享
  • value:信号量的初始值(计数器的初始值)

返回值:初始化信号量成功返回0,失败返回-1

注意:  POSIX信号量与System V信号量作用相同,都用于同步操作,达到无冲突的访问共享资源目的,但POSIX信号量可用于线程间同步

5.2.2 销毁信号量

int sem_destroy(sem_t *sem);

参数sem:需要销毁的信号量

返回值:销毁信号量成功返回0,失败返回-1

5.2.3 等待信号量

int sem_wait(sem_t *sem);

参数sem:需要等待的信号量

返回值:(P操作)

  • 等待信号量成功返回0,信号量的值减一
  • 等待信号量失败返回-1,信号量的值保持不变

5.2.4 发布信号量

int sem_post(sem_t *sem);

参数sem:需要发布的信号量

返回值:(V操作)

  • 发布信号量成功返回0,信号量的值加一
  • 发布信号量失败返回-1,信号量的值保持不变

六、二元信号量模拟互斥功能

信号量本质是计数器,若将信号量的初始值设置为1,那么此时该信号量被称为二元信号量。信号量的初始值为1,说明信号量所描述的临界资源只有一份,此时信号量的作用基本等价于互斥锁

在主线程中创建五个新线程,让这五个新线程执行抢票逻辑,并且每次抢完票后打印输出此时剩余的票数。用全局变量tickets记录当前剩余的票数,此时tickets是会被多个执行流同时访问的临界资源,在下面的代码中并没有对tickets进行任何保护操作

#include 
#include 
#include 
#include 

int tickets = 2000;
void* TicketGrabbing(void* arg)
{
	std::string name = (char*)arg;
	while (true){
		if (tickets > 0){
			usleep(1000);
			std::cout << name << " get a ticket, tickets left: " << --tickets << std::endl;
		}
		else{
			break;
		}
	}
	std::cout << name << " quit..." << std::endl;
	pthread_exit((void*)0);
}
int main()
{
	pthread_t tid[5];
	pthread_create(tid, nullptr, TicketGrabbing, (void*)"thread 1");
	pthread_create(tid + 1, nullptr, TicketGrabbing, (void*)"thread 2");
	pthread_create(tid + 2, nullptr, TicketGrabbing, (void*)"thread 3");
	pthread_create(tid + 3, nullptr, TicketGrabbing, (void*)"thread 4");
	pthread_create(tid + 4, nullptr, TicketGrabbing, (void*)"thread 5");
	
	pthread_join(tid[0], nullptr);
	pthread_join(tid[1], nullptr);
	pthread_join(tid[2], nullptr);
	pthread_join(tid[3], nullptr);
	pthread_join(tid[4], nullptr);
	return 0;
}

生产者消费者模型_第7张图片

运行代码后发现,线程打印输出剩余票数时出现了票数剩余为负数的情况,这并不符合预期

下面在抢票逻辑中加入二元信号量,让每个线程在访问全局变量tickets之前先申请信号量,访问完毕后再释放信号量,此时就达到了互斥的效果

#include 
#include 
#include 
#include 
#include 
class Sem{
public:
	Sem(int num) { sem_init(&_sem, 0, num); }
	~Sem() { sem_destroy(&_sem); }
	void P() { sem_wait(&_sem); }
	void V() { sem_post(&_sem); }
private:
	sem_t _sem;
};
static Sem sem(1);
int tickets = 2000;
void* TicketGrabbing(void* arg)
{
	std::string name = (char*)arg;
	while (true){
        sem.P();
		if (tickets > 0){
			usleep(1000);
			std::cout << name << " get a ticket, tickets left: " << --tickets << std::endl;
            sem.V();
		}
		else{
            sem.V();
			break;
		}
	}
	std::cout << name << " quit..." << std::endl;
	pthread_exit((void*)0);
}
int main()
{
	pthread_t tid[5];
	pthread_create(tid, nullptr, TicketGrabbing, (void*)"thread 1");
	pthread_create(tid + 1, nullptr, TicketGrabbing, (void*)"thread 2");
	pthread_create(tid + 2, nullptr, TicketGrabbing, (void*)"thread 3");
	pthread_create(tid + 3, nullptr, TicketGrabbing, (void*)"thread 4");
	pthread_create(tid + 4, nullptr, TicketGrabbing, (void*)"thread 5");
	
	pthread_join(tid[0], nullptr);
	pthread_join(tid[1], nullptr);
	pthread_join(tid[2], nullptr);
	pthread_join(tid[3], nullptr);
	pthread_join(tid[4], nullptr);
	return 0;
}

生产者消费者模型_第8张图片

运行代码后就不会出现剩余票数为负的情况了,同一时刻只会有一个执行流对全局变量tickets进行访问,不会出现数据不一致的问题

七、基于RingQueue的生产者消费者模型

生产者消费者模型_第9张图片

7.1 空间资源与数据资源

生产者关注的是空间资源,消费者关注的是数据资源

对于生产者和消费者而言,关注的资源是不同的:

  • 生产者关注的是环形队列当中是否有空间(blank),只要有空间生产者就可以进行生产
  • 消费者关注的是环形队列当中是否有数据(data),只要有数据消费者就可以进行消费

_space_sem和_data_sem的初始值设置

用信号量来描述环形队列当中的空间资源(_space_sem)和数据资源(_data_sem),在初始信号量时设置的初始值是有所不同的:

  • _space_sem的初始值应该设置为环形队列的容量,因为刚开始时环形队列中全是空间
  • _data_sem的初始值应该设置为0,因为刚开始时环形队列当中没有数据

7.2 资源的申请与释放

生产者申请空间资源,释放数据资源

对于生产者来说,生产者每次生产数据前都需要先申请_space_sem:

  • 若_space_sem的值不为0,则信号量申请成功,此时生产者可以进行生产操作
  • 若_space_sem的值为0,则信号量申请失败,此时生产者需要在_space_sem的等待队列下进行阻塞等待,直到环形队列当中有新的空间后再被唤醒

当生产者生产完数据后,应释放_data_sem:

  • 虽然生产者在进行生产前是对_space_sem进行的P操作,但是当生产者生产完数据,应该对_data_sem进行V操作而不是_space_sem
  • 生产者在生产数据前申请到的是空间位置,当生产者生产完数据后,该位置当中存储的是生产者生产的数据,在该数据被消费者消费之前,该位置不再是blank位置,而是data位置
  • 当生产者生产完数据后,意味着环形队列当中多了一个data位置,因此应该对_data_sem进行V操作

消费者申请数据资源,释放空间资源

对于消费者来说,消费者每次消费数据前都需要先申请_data_sem:

  • 若_data_sem的值不为0,则信号量申请成功,此时消费者可以进行消费操作
  • 若_data_sem的值为0,则信号量申请失败,此时消费者需要在_data_sem的等待队列下进行阻塞等待,直到环形队列当中有新的数据后再被唤醒

当消费者消费完数据后,应该释放_space_sem:

  • 虽然消费者在进行消费前是对_data_sem进行的P操作,但是当消费者消费完数据,应该对_space_sem进行V操作
  • 消费者在消费数据前申请到的是data位置,当消费者消费完数据后,该位置当中的数据已经被消费过了,再次被消费就没有意义了,为了让生产者后续可以在该位置生产新的数据,应该将该位置算作blank位置,而不是data位置
  • 当消费者消费完数据后,意味着环形队列当中多了一个blank位置,因此应该对_space_sem进行V操作

7.3 两个规则

规则一:生产者和消费者不能对同一个位置进行访问

  • 若生产者和消费者访问的是环形队列当中的同一个位置,那么此时生产者和消费者就相当于同时对这一块临界资源进行了访问,这是不允许的
  • 若生产者和消费者访问的是环形队列当中的不同位置,那么此时生产者和消费者是可以同时进行生产和消费的,此时不会出现数据不一致等问题

生产者消费者模型_第10张图片

规则二:无论是生产者还是消费者,都不应该将对方套一个圈以上

  • 生产者从消费者的位置开始一直按顺时针方向进行生产,若生产者生产的速度比消费者消费的速度快,那么当生产者绕着消费者生产了一圈数据后再次遇到消费者,此时生产者就不应该再继续生产了,因为再生产就会覆盖还未被消费者消费的数据
  • 同理,消费者从生产者的位置开始一直按顺时针方向进行消费,若消费者消费的速度比生产者生产的速度快,那么当消费者绕着生产者消费了一圈数据后再次遇到生产者,此时消费者就不应该再继续消费了,因为再消费就会消费到缓冲区中保存的废弃数据

生产者消费者模型_第11张图片

7.4 代码实现

RingQueue就是生产者消费者模型当中的交易场所,可以用C++STL库中的vector进行模拟

#ifndef _RING_QUEUE_HPP_
#define _RING_QUEUE_HPP_

#include 
#include 
#include 
#include "sem.hpp"

const size_t g_default_num = 5;
template
class RingQueue
{
public:
    RingQueue(size_t default_num = g_default_num)
    :_ring_queue(default_num),_num(default_num),_producer_index(0),_consumer_index(0) 
    ,_space_sem(default_num),_data_sem(0) {
        pthread_mutex_init(&_producer_mtx,nullptr);
        pthread_mutex_init(&_consumer_mtx,nullptr);
    }

    ~RingQueue() {
        pthread_mutex_destroy(&_producer_mtx);
        pthread_mutex_destroy(&_consumer_mtx);
    }

    void Push(const T& in)
    {
        //先申请访问临界资源的权限,再申请锁
        //减小锁的粒度
        //若资源有多个(信号量 > 1),在因为没抢占到锁而阻塞之前,就可以申请到信号量
        _space_sem.P();
        pthread_mutex_lock(&_producer_mtx);
        _ring_queue[_producer_index++] = in;
        _producer_index %= _num;
        pthread_mutex_unlock(&_producer_mtx);
        _data_sem.V();
    }
    void Pop(T* out)
    {
        _data_sem.P();
        pthread_mutex_lock(&_consumer_mtx);
        *out = _ring_queue[_consumer_index++];
        _consumer_index %= _num;
        pthread_mutex_unlock(&_consumer_mtx);
        _space_sem.V();
    }
    void Debug() { 
        std::cerr << "size:" << _ring_queue.size() << std::endl; 
        std::cerr << "num:" << _num << std::endl; 
    }
private:
    std::vector _ring_queue;
    int _num;
    int _producer_index;
    int _consumer_index;
    Sem _space_sem;
    Sem _data_sem;
    pthread_mutex_t _producer_mtx;
    pthread_mutex_t _consumer_mtx;
};
#endif
  • 当不设置环形队列的大小时,默认将环形队列的容量上限设置为5
  • 代码中的RingQueue是用vector模拟的,生产者每次生产的数据放到vector下标为_producer_index的位置,消费者每次消费的数据来源于vector下标为_consumer_index的位置
  • 生产者每次生产数据后_producer_index都会进行++,标记下一次生产数据的存放位置,++后的下标会与环形队列的容量进行取模运算,实现"环形"的效果
  • 消费者每次消费数据后_consumer_index都会进行++,标记下一次消费数据的来源位置,++后的下标会与环形队列的容量进行取模运算,实现"环形"的效果
  • _producer_index只会由生产者线程进行更新,_consumer_index只会由消费者线程进行更新

生产者线程不断生产数据放入环形队列,消费者线程不断从环形队列里取出数据进行消费

#include "ringQueue.hpp"
#include 
#include 
#include 
#include 
void* Producer(void* arg)
{
    RingQueue* rq = (RingQueue*)arg;
    while(true)
    {
        sleep(1);
        //构建数据或任务对象(一般从外部获取)
        int date = rand() % 100 + 1;
        //Push入环形队列
        rq->Push(date);
        std::cout << "生产:" << date << "[" << pthread_self() << "]" <* rq = (RingQueue*)arg;
    while(true)
    {
        sleep(1);
        //从环形队列中读取数据或任务
        int date = 0;
        rq->Pop(&date);
        //处理数据或任务
        std::cout << "消费:" << date << "[" << pthread_self() << "]" <* rq = new RingQueue();
    // rq->Debug();
    pthread_t producer[3],consumer[2];
    pthread_create(producer,nullptr,Producer,rq);
    pthread_create(producer + 1,nullptr,Producer,rq);
    pthread_create(producer + 2,nullptr,Producer,rq);
    pthread_create(consumer,nullptr,Consumer,rq);
    pthread_create(consumer + 1,nullptr,Consumer,rq);

    for(int i = 0;i < 3; ++i) pthread_join(producer[i],nullptr);
    for(int i = 0;i < 2; ++i) pthread_join(consumer[i],nullptr);
    return 0;
}
  • 环形队列要让生产者线程向队列中Push数据,让消费者线程从队列中Pop数据,因此这个环形队列必须要让这两个线程同时看到,所以在创建生产者线程和消费者线程时,需要将环形队列作为线程执行例程的参数进行传入
  • 代码中生产者生产数据就是将获取到的随机数Push到环形队列,而消费者就是从环形队列Pop数据,为了便于观察,可以将生产者生产的数据和消费者消费的数据进行打印输出

将信号量进行封装 

#ifndef _SEM_HPP
#define _SEM_HPP

#include 
#include 
class Sem
{
public:
    Sem(size_t value) { sem_init(&_sem,0,value); }
    ~Sem() { sem_destroy(&_sem); }
    void P() { sem_wait(&_sem); }
    void V() { sem_post(&_sem); }
private:
    sem_t _sem;
};
#endif

7.5 信号量维护环形队列的原理

在_space_sem和_data_sem两个信号量的保护,该环形队列中不可能会出现数据不一致的问题

只有当生产者和消费者指向同一个位置并访问时,才会导致数据不一致的问题,而此时生产者和消费者在对环形队列进行写入或读取数据时,只有两种情况会指向同一个位置:

  • 环形队列为空时
  • 环形队列为满时

但在这两种情况下,生产者和消费者不会同时对环形队列进行访问:

  • 当环形队列为空的时,消费者一定不能进行消费,因为此时数据资源为0
  • 当环形队列为满的时,生产者一定不能进行生产,因为此时空间资源为0

即当环形队列为空和满时,已经通过信号量保证了生产者和消费者的串行化过程。而除了这两种情况之外,生产者和消费者指向的都不是同一个位置,因此该环形队列当中不可能会出现数据不一致的问题。并且大部分情况下生产者和消费者指向并不是同一个位置,因此大部分情况下该环形队列可以让生产者和消费者并发的执行

你可能感兴趣的:(Linux系统,linux)