在实际的软件开发过程中,有些模块专门负责产生数据,另一个相对应的模块负责处理数据。在这种情况下,可以形象地称产生数据的模块为生产者,消费数据的模块为消费者。
往往实际中的生产者和消费者不仅仅是这样的,在它们之间还存在着一个缓冲区作为中介。生产者把数据生产出来放入缓冲区,消费者从缓冲区取得数据消费。
也就是说生产者和消费者有“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”原则——三种关系,两个角色,一个交易场所,一切代码的实现都是基于这个原则的。