【Linux】生产者消费者模型

一、概念


生产者:产生数据的的模块。
消费者:处理数据的模块。
那么生产者产生数据之后,消费者怎么拿,从哪拿呢?所以,仅仅有这两个角色是不能完成我们期望的工作的。还需要一个缓冲区,就像超市的货架一样,供货商(生产者)将商品摆到货架(缓冲区)上,购买者(消费者)从货架上拿走,这个货架也是必不可少的。
试想如果没有缓冲区,生产者生产一个数据之后,就必须等待消费者消费完成,生产者才能继续生产,那么如果生产者很快,而消费者很慢,那么就只能让生产者干等。这就好比让CPU和外设直接打交道,如果没有缓存,还不得慢死,白白浪费CPU时间。
抽象出此模型如下:
这里写图片描述
接下来说明三种关系:
1.生产者与生产者之间:就像供货商与供货商之间,存在明显的竞争关系,在操作系统上,我们称为互斥关系。
2.消费者与消费者之间:同样是互斥的。
3.生产者与消费者之间:首先必须保证,生产者在生产时,消费者不能来打扰,否则会出现数据二义性的问题。(生产者要往缓冲区里写入“1234”,刚写了12时消费者就来缓冲区取数据了,那么它拿到的只能是12)。同时,消费者在消费时生产者也不能打扰,道理是一样的。所以,它们之间也有互斥的关系。除了互斥外,还必须保证生产者消费者按照一定的顺序访问资源。因为必须是让生产者先生产,消费者才能进行消费,类似于这样按照顺序访问资源成为同步,所以这两者之间还有同步的关系。

二、模型模拟


1.一个生产者一个消费者。

我们使用两个线程分别模拟生产者和消费者,使用单链表作为缓冲区,每次让生产者PUSH数据到链表头,消费者也每次从链表头部取POP数据。
完整的代码:

#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
pthread_mutex_t mylock=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t mycond=PTHREAD_COND_INITIALIZER;
typedef struct node
{
    int _data;
    struct node* _next;
}node,*pnode;

typedef struct Linklist
{
    node * phead;
}Linklist,*pLinklist;

pnode creatNode(int data)
{
    pnode newnode=(pnode)malloc(sizeof(node));
    if(newnode==NULL)
    {
        perror("malloc");
        exit(EXIT_FAILURE);
    }
    newnode->_data=data;
    newnode->_next=NULL;
    return newnode;
}

void initList(pLinklist plist)
{
    assert(plist);
    //pLinklist head=(pLinklist)malloc(sizeof(Linklist));
//  plist=head;
    plist->phead=NULL;

}


void pushHead(pLinklist list,int data)
{
    assert(list);
    pnode newnode=creatNode(data);
    if(list->phead==NULL)
    {
        list->phead=newnode;
        return;
    }
    newnode->_next=list->phead;
    list->phead=newnode;
}


void popHead(pLinklist list,int* data)
{
    assert(list);
    if(list->phead==NULL)
    {
        printf("list empty!\n");
        return;
    }
    pnode del=list->phead;
    *data=del->_data;
    list->phead=del->_next;
    del->_next=NULL;
    free(del);
}

void destoryList(pLinklist list)
{
    assert(list);
    if(list->phead!=NULL)
    {
        pnode cur =list->phead;
        while(cur)
        {
            pnode del=cur;
            cur=cur->_next;
            free(del);
            del=NULL;
        }
    }
    list->phead=NULL;

}

void showList(pLinklist list)
{
    assert(list);
    pnode cur=list->phead;
    while(cur!=NULL)
    {
        printf("%d ",cur->_data);
        cur=cur->_next;
    }
    printf("\n");
}
void* producter_thread(void* arg)
{
    pLinklist list=(pLinklist)arg;
    while(1)
    {

        sleep(1);
        pthread_mutex_lock(&mylock);   //访问临界区前加锁
        pushHead(list,rand()%1000);
        pthread_cond_signal(&mycond);   //生产完毕唤醒等待在该条件变量下的线程
        pthread_mutex_unlock(&mylock);  //访问结束解锁
        printf("producter success %d\n",list->phead->_data);
    }
}

void* consumer_thread(void* arg)
{
    pLinklist list=(pLinklist)arg;
    while(1)
    {
        sleep(1);
        pthread_mutex_lock(&mylock);  //加锁
        int data=0;
        while(list->phead==NULL)
        {
            pthread_cond_wait(&mycond,&mylock);  //若链表中无数据,则消费者需要等待。
        }
        popHead(list,&data);
        pthread_mutex_unlock(&mylock);    //解锁
        printf("consumer success %d\n",data);
    }
}
int main()
{
    Linklist list;
    initList(&list);
    pthread_t tid1,tid2;
    pthread_create(&tid1,NULL,producter_thread,(void*)&list);//创建线程
    pthread_create(&tid2,NULL,consumer_thread,(void*)&list);

    pthread_join(tid1,NULL);   //等待线程结束回收线程
    pthread_join(tid2,NULL);

    destoryList(&list);
    return 0;
}

代码中用到了互斥锁和条件变量。简单的介绍一下:
互斥锁

为了实现生产者与消费者之间的互斥关系,我们用到了互斥锁。你可以就把他想象成一把锁,只有拿到这把锁的人才能访问资源,而拿不到锁只能等待,直到锁资源被释放。

pthread_mutex_t   

用它可以声明一个锁,再初始化它,如果像我们上面的代码一样,mutex变量时全局的,或者静态的,可以⽤宏定义PTHREAD_MUTEX_INITIALIZER来初始化。否则使用函数初始化,初始化函数:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);

函数具体怎么用可以在Linux下使用:man 函数名。查看函数用法和详细信息。
加锁函数:

int pthread_mutex_lock(pthread_mutex_t *mutex);

线程可以使用它获得mutex,如果当前mutex已经被其他线程申请到了,那么当前线程只能挂起等待,知道其他线程释放mutex。释放mutex的函数:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

如果⼀个线程既想获得锁,又不想挂起等待,可以调⽤:

int pthread_mutex_trylock(pthread_mutex_t *mutex);

如果Mutex已经被另⼀个线程获得,这个函数会失败返回EBUSY,⽽不会使线程挂起等待。
条件变量

在上面的代码中,我们使用条件变量保证了生产者和消费者之间的同步。怎么做到的呢?

pthread_cond_t    //声明条件变量

初始化和mutex一样,可分别用宏或者函数。

int pthread_cond_init(pthread_cond_t *restrict cond,
              const pthread_condattr_t *restrict attr);
//初始化
int pthread_cond_wait(pthread_cond_t *restrict cond,
              pthread_mutex_t *restrict mutex);

⼀个线程可以调⽤
pthread_cond_wait在⼀个Condition Variable上阻塞等待,这个函数做以下三步操作:
1>释放Mutex

2> 阻塞等待

3>当被唤醒时,重新获得Mutex并返回。
上面的代码我们在链表中无数据时wait。在生产者生产完数据时,使用
pthread_cond_signal唤醒在Condition Variable上等待的消费者线程。

 int pthread_cond_signal(pthread_cond_t *cond);

2.多生产者多消费者。

要模拟此模型,我们就要多考虑两种关系,即前面提到的,保证生产者与生产者,消费者与消费者之间的互斥关系。除此之外,使用一个环形Buffer作为缓冲区。
这里写图片描述
生产者关心的是有无空的格子,消费者关心有无数据。所以,这个模型里面我们用到了信号量,用它来表示格子和数据的数量。

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<semaphore.h>
#include<stdlib.h>
#define _SIZE_ 64     //环形队列大小64
sem_t blank;    //格子
sem_t data;     //数据
sem_t pro_lock;    //保证生产者之间互斥的信号量
sem_t con_lock;     //保证消费者之间互斥的信号量
int ring[_SIZE_];
void* pro_run(void* arg)
{
    pthread_detach(pthread_self());
    static int i=0;
    int id=(int)arg;
    while(1)
    {
        sleep(1);
//      usleep(1000);
        sem_wait(&blank);
        sem_wait(&pro_lock);
        int num=rand()%1000;
        ring[i++]=num;
        printf("生产者%d生产:%d,tid:%lu\n",id,num,pthread_self());
        i%=_SIZE_;
        sem_post(&pro_lock);
        sem_post(&data);
    }
}


void* con_run(void* arg)
{
    pthread_detach(pthread_self());
    static int i=0;
    int id=(int)arg;
    while(1)
    {
//      sleep(1);
        usleep(1000);
        sem_wait(&data);
        sem_wait(&con_lock);
        printf("消费者%d消费:%d,tid:%lu\n:",id,ring[i++],pthread_self());
        i%=_SIZE_;
        sem_post(&con_lock);
        sem_post(&blank);
    }
}
int main()
{
    pthread_t producter,consumer,producter1,consumer1;
    sem_init(&blank,0,_SIZE_);
    sem_init(&data,0,0);
    sem_init(&pro_lock,0,1);
    sem_init(&con_lock,0,1);
    int i=0;
    pthread_create(&producter,NULL,pro_run,(void*)i);
    pthread_create(&consumer,NULL,con_run,(void*)i);
    i++;
    pthread_create(&producter1,NULL,pro_run,(void*)i);
    pthread_create(&consumer1,NULL,con_run,(void*)i);
    sem_destroy(&blank);
    sem_destroy(&data);
    pthread_join(producter,NULL);
    pthread_join(consumer,NULL);
    return 0;
}

semaphore(信号量)变量的类型为sem_t。
这里写图片描述
sem_wait相当于P操作,所以,生产者每次wait的是格子信号量,sem_post相当于V操作,所以,生产者生产完之后post的是数据信号量。
消费者wait数据,post格子。再使用两个初值为1的信号量分别为生产者与生产者之间,消费者与消费者之间加锁。我们先试试让消费者比生产者快一些(消费者相对生产者睡眠短一点),看会不会有问题。
这里写图片描述
可见,虽然消费者比较快,但是也只能等生产者生产了它才能消费。
再调整代码使生产者比消费者快,会出现什么现象呢?
这里写图片描述
生产者一瞬间将缓冲队列生产满,然后等待消费者进程消费腾出格子后,它才能继续生产。这些都是我们的信号量保证的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值