一、概念
生产者:产生数据的的模块。
消费者:处理数据的模块。
那么生产者产生数据之后,消费者怎么拿,从哪拿呢?所以,仅仅有这两个角色是不能完成我们期望的工作的。还需要一个缓冲区,就像超市的货架一样,供货商(生产者)将商品摆到货架(缓冲区)上,购买者(消费者)从货架上拿走,这个货架也是必不可少的。
试想如果没有缓冲区,生产者生产一个数据之后,就必须等待消费者消费完成,生产者才能继续生产,那么如果生产者很快,而消费者很慢,那么就只能让生产者干等。这就好比让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的信号量分别为生产者与生产者之间,消费者与消费者之间加锁。我们先试试让消费者比生产者快一些(消费者相对生产者睡眠短一点),看会不会有问题。
可见,虽然消费者比较快,但是也只能等生产者生产了它才能消费。
再调整代码使生产者比消费者快,会出现什么现象呢?
生产者一瞬间将缓冲队列生产满,然后等待消费者进程消费腾出格子后,它才能继续生产。这些都是我们的信号量保证的。