我们来认识一下一个模型:生产者消费者模型。在实际的软件开发过程中,可能遇到这种情况:一个模块负责产生数据,另一个模块负责处理数据。注:这里的模块是广义上的模块,可能是进程,线程,类等。产生数据的模块是生产者,处理数据的模块是消费者。但是还不够得上生产者消费者模型,这里还需要一个缓冲区,处于生产者消费者中间,作为中介,生产者往缓冲区生产数据,消费者从缓冲区消费数据。
缓冲区的优势:
解耦:假设生产者,消费者是两个类,如果生产者直接调用消费者,那么生产者会对消费者产生依赖关系,如果消费者代码出现变化,则很可能会影响到生产者。而它俩中间都依赖缓冲区,两者之间不直接解除,耦合也就降低了。
支持并发:由于函数是同步的,在消费者的方法还没有返回之前,生产者只能等待,消费者的效率直接影响到了生产者的生产。而支持并发就是它俩是独立的并发主体,生产者生产数据后往缓冲区一丢,就继续生产,基本上不依赖消费者的处理速度。
支持忙闲不均:如果生产者生产速度时快时慢,缓冲区优势就凸显出来了:快时消费者来不及处理,就丢到缓冲区,等生产速度降下来时,再慢慢处理。
我们来实现基于单链表的生产者消费者模型,我们先看下面代码,模拟生产消费过程
#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_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //定义锁
static node_p alloc_node(int _d)
{
node_p tmp = (node_p)malloc(sizeof(node_t));
if(!tmp)
{
perror("malloc");
exit(1);
}
tmp->data = _d;
tmp->next = NULL;
}
static void delete_node(node_p _n)
{
if(_n != NULL)
{
free(_n);
}
}
static int isEmpty(node_p _h)
{
return (NULL == _h->next) ? 1:0;
}
void initList(node_pp _h)
{
*_h = alloc_node(0);
}
void pushFront(node_p _h,int _d)
{
node_p tmp = alloc_node(_d);
tmp->next = head->next;
head->next = tmp;
}
void popFront(node_p _h,int* _out)
{
if(!isEmpty(_h))
{
node_p p = _h->next;
_h->next = p->next;
*_out = p->data;
delete_node(p);
}
}
void destroyList(node_p _h)
{
int out = 0;
while(!isEmpty(_h))
{
popFront(_h,&out);
}
delete_node(_h);
}
void showList(node_p _h)
{
node_p start = _h->next;
while(start)
{
printf("%d ",start->data);
start = start->next;
}
printf("\n");
}
void* product(void* arg) //生产者
{
while(1)
{
int p = rand()%1234;
pthread_mutex_lock(&lock); //加锁
pushFront(head,p); //临界区
pthread_mutex_unlock(&lock); //解锁
printf("product done:%d\n",p);
sleep(1);
}
}
void* consume(void* arg) //消费者
{
int c;
while(1){
c = -1;
pthread_mutex_lock(&lock);
while(!isEmpty(head)){
popFront(head,&c);
}
pthread_mutex_unlock(&lock);
printf("consume done:%d\n",c);
}
}
int main()
{
initList(&head);
int i = 0;
for(;i<10;i++){
pushFront(head,i);
showList(head);
sleep(1);
}
int out = 0;
for(;i>5;i--){
popFront(head,&out);
showList(head);
sleep(1);
}
destroyList(head);
return 0;
}
我们来看运行结果:
由于生产者生产数据之后sleep一秒,这时就造成一种持续访问的现象,消费者就会一直访问资源(链表),导致生产者长时间无法访问资源,这种情况我们应该是要规避的。所以我们就需要消费者在资源中无数据时等待,等生产者产生数据后再通知他进行访问,所以引入了一个新的概念:条件变量。条件变量使线程可以睡眠等待一个条件的出现。它是利用线程间共享的全局变量来进行同步的一种机制,主要有两个动作:一个线程等待“条件变量的条件成立”而等待;另一线程使“条件成立”(给出条件成立信号)。为防止竞争,条件变量的使用总是和互斥锁结合在一起使用。
#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_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //定义锁
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //定义条件变量
static node_p alloc_node(int _d)
{
node_p tmp = (node_p)malloc(sizeof(node_t));
if(!tmp)
{
perror("malloc");
exit(1);
}
tmp->data = _d;
tmp->next = NULL;
}
static void delete_node(node_p _n)
{
if(_n != NULL)
{
free(_n);
}
}
static int isEmpty(node_p _h)
{
return (NULL == _h->next) ? 1:0;
}
void initList(node_pp _h)
{
*_h = alloc_node(0);
}
void pushFront(node_p _h,int _d)
{
node_p tmp = alloc_node(_d);
tmp->next = head->next;
head->next = tmp;
}
void popFront(node_p _h,int* _out)
{
if(!isEmpty(_h))
{
node_p p = _h->next;
_h->next = p->next;
*_out = p->data;
delete_node(p);
}
}
void destroyList(node_p _h)
{
int out = 0;
while(!isEmpty(_h))
{
popFront(_h,&out);
}
delete_node(_h);
}
void showList(node_p _h)
{
node_p start = _h->next;
while(start)
{
printf("%d ",start->data);
start = start->next;
}
printf("\n");
}
void* product(void* arg) //生产者
{
while(1)
{
int p = rand()%1234;
pthread_mutex_lock(&lock); //加锁
pushFront(head,p); //临界区
pthread_mutex_unlock(&lock); //解锁
pthread_cond_signal(&cond); //发送一个信号给另外一个正处于阻塞等待状态的线程(本例中的消费者),使其脱离阻塞状态,继续执行,如果没有线程处于阻塞状态,也会成功返回。
printf("product done:%d\n",p);
sleep(1);
}
}
void* consume(void* arg) //消费者
{
int c;
while(1){
c = -1;
pthread_mutex_lock(&lock);
while(isEmpty(head)){
printf("consume begin waiting\n");
pthread_cond_wait(&cond,&lock); //消费失败,在cond等待,并且释放锁lock,等再次被唤醒时,重新获得锁
}
popFront(head,&c);
pthread_mutex_unlock(&lock);
printf("consume done:%d\n",c);
}
}
int main()
{
initList(&head);
pthread_t p,c;
pthread_create(&p,NULL,product,NULL);
pthread_create(&c,NULL,consume,NULL);
pthread_join(p,NULL); //线程默认是可结合的,所以线程等待
pthread_join(c,NULL);
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
return 0;
}
定义两个条件变量,使其互相通信
void* product(void* arg)
{
while(1)
{
int p = rand()%1234;
pthread_mutex_lock(&lock);
while(!isEmpty(head)){
printf("product begin waiting\n");
pthread_cond_wait(&cond2,&lock);
}
pushFront(head,p);
pthread_mutex_unlock(&lock);
pthread_cond_signal(&cond); //p->c
printf("product done:%d\n",p);
//sleep(1);
}
}
void* consume(void* arg)
{
int c;
while(1){
c = -1;
pthread_mutex_lock(&lock);
while(isEmpty(head)){
printf("coonsume begin waiting\n");
pthread_cond_wait(&cond,&lock);
}
popFront(head,&c);
pthread_cond_signal(&cond2); //c->p
pthread_mutex_unlock(&lock);
printf("consume done:%d\n",c);
sleep(1);
}
}
基于环形队列的生产者消费者模型
基于环形队列的模型类似进程间通信使用信号量的方法,生产者生产数据时,需要P操作(申请格子资源),释放的时候即生产数据之后V操作。而消费者拿数据时进行P操作(申请数据资源),释放的时候即拿走数据之后V操作。
这里必须满足下面条件:
1,消费者一直在生产者后边;
2,生产者不能给消费者套圈;
3,两者不能同时出现在一个格子中,除非队列满了或队列为空。满时,消费者先走,空时,生产者先走。
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
#define SIZE 64
int ring[SIZE];
sem_t blank_sem;
sem_t data_sem;
void *product(void *arg)
{
int step = 0;
int data = 0;
while(1)
{
sem_wait(&blank_sem);
ring[step++] = data;
sem_post(&data_sem);
step %= SIZE;
printf("Product done:%d\n",data++);
sleep(1);
}
}
void *consum(void *arg)
{
int step = 0;
while(1)
{
sleep(1);
sem_wait(&data_sem);
int data = ring[step++];
sem_post(&blank_sem);
step %= SIZE;
printf("comsum done:%d\n",data);
}
}
int main()
{
sem_init(&blank_sem,0,SIZE);
sem_init(&data_sem,0,0);
pthread_t p,c;
pthread_create(&p,NULL,product,NULL);
pthread_create(&c,NULL,consum,NULL);
pthread_join(p,NULL);
pthread_join(c,NULL);
sem_destroy(&blank_sem);
sem_destroy(&data_sem);
return 0;
}
改成多生产者多消费者模型
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
#define SIZE 64
int ring[SIZE];
sem_t blank_sem;
sem_t data_sem;
pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;
void *product1(void *arg)
{
int step = 0;
int data = 0;
while(1)
{
pthread_mutex_lock(&lock1);
sem_wait(&blank_sem);
ring[step++] = data;
sem_post(&data_sem);
pthread_mutex_unlock(&lock1);
step %= SIZE;
printf("product1 done:%d\n",data++);
sleep(1);
}
}
void *product2(void *arg)
{
int step = 0;
int data = 0;
while(1)
{
pthread_mutex_lock(&lock1);
sem_wait(&blank_sem);
ring[step++] = data;
sem_post(&data_sem);
pthread_mutex_unlock(&lock1);
step %= SIZE;
printf("product2 done:%d\n",data++);
sleep(1);
}
}
void *consum1(void *arg)
{
int step = 0;
while(1)
{
sleep(1);
pthread_mutex_lock(&lock2);
sem_wait(&data_sem);
int data = ring[step++];
sem_post(&blank_sem);
pthread_mutex_unlock(&lock2);
step %= SIZE;
printf("consum1 done:%d\n",data);
}
}
void* consum2(void* arg)
{
int step = 0;
while(1)
{
sleep(1);
pthread_mutex_lock(&lock2);
sem_wait(&data_sem);
int data = ring[step++];
sem_post(&blank_sem);
pthread_mutex_unlock(&lock2);
step %= SIZE;
printf("consum2 done:%d\n",data);
}
}
int main()
{
sem_init(&blank_sem,0,SIZE);
sem_init(&data_sem,0,0);
pthread_t p1,p2,c1,c2;
pthread_create(&p1,NULL,product1,NULL);
pthread_create(&p2,NULL,product2,NULL);
pthread_create(&c1,NULL,consum1,NULL);
pthread_create(&c2,NULL,consum2,NULL);
pthread_join(p1,NULL);
pthread_join(p2,NULL);
pthread_join(c1,NULL);
pthread_join(c2,NULL);
sem_destroy(&blank_sem);
sem_destroy(&data_sem);
return 0;
}
注:
调用pthread_cond_wait()之前必须获得锁。它是对互斥量做了操作,首先将解锁和挂起线程作为原子操作执行,pthread_cond_signal()即可获得互斥量。当pthread_cond_wait()返回时,又会自动加锁,所以其效果相当于持有的锁的状态未发生改变。
而一个阻塞线程被唤醒可能是被pthread_cond_signal()/pthread_cond_broadcast()唤醒,也可能是在被信号中断后唤醒(造成假唤醒)。所以pthread_cond_wait()的返回并不意味着条件的值发生了改变(假唤醒),也可能是函数出错返回,因此必须注意因此必须重新检查条件的值,比如使用while()。
而条件变量的使用必须结合互斥锁的使用。否则对条件变量的通知可能发生在等待条件变量之前,从而造成死锁。导致死锁的最常见错误是自死锁或递归死锁。在自死锁或递归死锁中,线程尝试获取已被其持有的锁。