【每天一点linux】多线程编程之生产者消费者模型

在实际的软件开发过程中,有些模块专门负责产生数据,另一个相对应的模块负责处理数据。在这种情况下,可以形象地称产生数据的模块为生产者,消费数据的模块为消费者。

往往实际中的生产者和消费者不仅仅是这样的,在它们之间还存在着一个缓冲区作为中介。生产者把数据生产出来放入缓冲区,消费者从缓冲区取得数据消费。

这里写图片描述

也就是说生产者和消费者有“321”原则。

3 — 三种关系
2 — 二种角色
1 — 一个交易场所

先说三种关系。
生产者 & 消费者 —同步互斥关系
生产者和消费者在同一时间内,一个生产,一个消费,但只能有一个可以占有临界资源,不可能两个都拿到临界资源。

生产者 & 生产者 —互斥关系
对消费者来说,在某个时间内只能消费一个生产者生产的数据,所以生产者和生产者之间也是互斥的。

消费者 & 消费者 —互斥关系
同上。

接着说两种关系。在这个模型中,只会有一个负责生产,一个负责消费,所以不会存在第三种关系。

最后,一个交易场所。很明显,对生产者和消费者来说,它们的交易场所只会是缓冲区。

基于链表实现生产者消费者模型

在这里,我们用我们学过的数据结构中的链表来模拟缓冲区,生产者每生产一个节点,就在链表上开辟一个新节点;消费者每消费一个数据,就删除一个数据。我们选用带头节点的单链表来模拟缓冲区,采用头插头删的方式进行生产消费。整个模拟图如下。

这里写图片描述

在实现代码前,我们需要说明几个问题。生产者和消费者的关系表明,临界资源不可能被两个人同时占有,所以为了避免临界资源被抢占,我们在这里需要引入互斥锁。

也就是说获得互斥锁的线程,可以占用临界资源完成“读—修改—写”操作,完成操作后释放锁,没有获得锁的线程只能等待。

#include< pthread.h>
int pthread_ mutex_ init ( pthread_mutex_t * restrict mutex, const pthread_ mutexattr_t * restrict attr);

int pthread_mutex_destroy(pthread_mutex_t *mutex);

这两个接口分别用来初始化互斥锁和销毁互斥锁。成功返回0,失败返回错误号。说完互斥锁,我们必须要了解下怎么给互斥锁加锁去掉锁。一般来说有两种加锁方式,阻塞式和非阻塞式。阻塞式是指结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞式指在不能立刻得到结果之前,不会阻塞当前线程。

#include < pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex); //阻塞式
int pthread_mutex_trylock(pthread_mutex_t *mutex); //非阻塞式
int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁

说完这个,代码实现就很简单了。当生产者/消费者在生产/消费的时候,我们对其加锁,当其生产/消费完了的时候,我们再进行解锁,让其他人使用。

#include<stdio.h>
#include<stdlib.h>
#include <pthread.h>
typedef struct node
{
    int data;
    struct node* next;
}node_t,*node_p,**node_pp;

node_p head=NULL;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;


node_p AllocNode(int d,node_p node)
{
    node_p n=(node_p)malloc(sizeof(node_t));
    if(!n)
    {
        perror("malloc");
        exit(1);
    }

    n->data=d;
    n->next=node;
    return n;

}

void InitList(node_pp _h)
{
    *_h=AllocNode(0,NULL);
}

int IsEmpty(node_p n)
{
    return n->next==NULL?1:0;
}

void FreeNode(node_p n)
{
    if(n!=NULL)
    {
        free(n);
        n=NULL;
    }
}

void PushFront(node_p n,int d)
{
    node_p tmp=AllocNode(d,NULL);
    tmp->next=n->next;
    n->next=tmp;
}

void PopFront(node_p n,int* out)
{
    if(!IsEmpty(n))
    {
        node_p tmp=n->next;
        n->next=tmp->next;
        *out=tmp->data;
        FreeNode(tmp);
    }
}

void ShowList(node_p n)
{
    node_p begin=n->next;
    while(begin)
    {
        printf("%d ",begin->data);
        begin=begin->next;
    }
    printf("\n");
}

void Destory(node_p n)
{
    int data;
    while(!IsEmpty(n))
    {
        PopFront(n,&data);
    }
    FreeNode(n);
}

void* Consumer(void* arg)
{
    int data=0;
    while(1)
    {
        pthread_mutex_lock(&lock);
        while(IsEmpty(head))
        {
            pthread_cond_wait(&cond,&lock);
        }
        PopFront(head,&data);
        printf("consumer done: %d\n",data);
        pthread_mutex_unlock(&lock);
    }
}

void* Product(void* arg)
{
    int data=0;
    while(1)
    {
        pthread_mutex_lock(&lock);
        data=rand()%1234;
        PushFront(head,data);
        printf("productor done:%d\n",data);
        pthread_mutex_unlock(&lock);
        pthread_cond_signal(&cond);
        sleep(1);
    }
}

int main()
{
    pthread_mutex_init(&lock,NULL);

    InitList(&head);
    pthread_t consumer,productor;
    pthread_create(&consumer,NULL,Consumer,NULL);
    pthread_create(&productor,NULL,Product,NULL);

    pthread_join(consumer,NULL);
    pthread_join(productor,NULL);

    Destory(head);
    pthread_mutex_destroy(&lock);
    pthread_cond_destroy(&cond);
    return 0;
}

结果如下。我们可以明显看出生产者生产一个,消费者消费一个。

这里写图片描述

在上面的模型中,假设生产者一直生产,消费者不进行消费,整个程序还是可以执行的,直到内存全部满。事实上,在生产消费的时候, 资源的可用数量是一定,也就是说会存在下面的情况。

  • 生产者必须先生产,消费者才能后消费。
  • 不存在未生产就消费的行为。
  • 当前资源已满,消费者未消费,生产者将无法生产。

这里写图片描述

我们可以将这种模型称为环形队列模型,实际,这种模型的底层我们是用数组实现的。对生产者来说,它只有得到空余的格子可以放数据时,它才会让消费者进行消费,同理对消费者。

在这里涉及到的是PV操作。PV操作是由P操作原语和V操作原语组组成的。

V原语的主要操作是:
⑴sem加1;
⑵若相加结果大于零,则进程继续执行;
⑶若相加结果小于或等于零,则唤醒一阻塞在该信号量上的进程,然后再返回原进程继续执行或转进程调度。

P操作的原语恰好与V原语相反。将信号量sem减1;如果sem >=0继续执行,否则就挂起等待,直到有进程释放,才会被唤醒。

老规矩,说一下我们的接口。

#include < semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value); //对可用资源初始化
int sem_destroy( sem_t *sem); //销毁可用资源
int sem_post(sem_t *sem); //V操作,释放资源
int sem_wait(sem_t *sem); //P操作,获得资源

接下来代码实现不用多说,整体的思路参照上面所说的。

#include<stdio.h>
#include<stdlib.h>
#include<semaphore.h>

int buf[64];
sem_t blacksem;
sem_t datasem;

void consume(void* arg)
{
    int step=0;
    while(1)
    {
        sem_wait(&datasem);
        int data=buf[step];
        printf("consumer : %d\n",data);
        step++;
        step%=64;
        sem_post(&blacksem);
    }
}

void product(void* arg)
{
    int step=0;
    while(1)
    {
        sem_wait(&blacksem);
        int data=rand()%1234;
        buf[step]=data;
        printf("product : %d\n",data);
        step++;
        step%=64;
        sem_post(&datasem);
        sleep(1);
    }
}

int main()
{

    sem_init(&blacksem,0,64);
    sem_init(&datasem,0,0);
    pthread_t productor,consumer;

    pthread_create(&consumer,NULL,consume,NULL);
    pthread_create(&productor,NULL,product,NULL);

    pthread_join(productor,NULL);
    pthread_join(consume,NULL);

    sem_destroy(&blacksem);
    sem_destroy(&datasem);
    return 0;
}

结果如预期,见图。

这里写图片描述

总结一下:在生产者消费者模型中,一定要把握好“321”原则——三种关系,两个角色,一个交易场所,一切代码的实现都是基于这个原则的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值