背景
某个模块负责产生数据,而这些数据由另一个模块来负责处理。那么产生数据的模块就称为生产者,处理数据的模块就称为消费者。
单单有生产者和消费者还不能称为生产者-消费者模型,我们还需要一个交易的场所(缓冲区),用自己的话总结来说,生产者-消费者模型可以称为“三二一原则”:
“三”指三种关系,1)生产者与生产者之间为互斥关系;
2)生产者与消费者之间为同步与互斥的关系;
3)消费者与消费者之间为互斥关系。
“二”指两种角色:生产者与消费者。
“一”指一个交易场所。
举一个帮助理解的寄信的例子,假设你要寄一封平信,大致过程如下:
1)你把信写号——相当于生产者制造数据;
2)你把信放入邮筒——相当于生产者把数据放入缓冲区;
3)邮递员把信从邮筒取出——相当于消费者把数据取出缓冲区;
4)邮递员把信拿去邮局做相应的处理——相当于消费者处理数据。
可能有人会有疑问为什么需要用到缓冲区呢?那么通过寄信的例子就可以很容易理解了,如果不需要邮筒,每个人寄信都要亲自将信直接交给邮递员,那么基本上就需要每个人都配有一个邮递员了。
缓冲区类型
队列缓冲区
单个生产者对应单个消费者。
在线程方式下,生产者和消费者各自为一个线程。生产者把数据写入队列头(以下简称push),消费者从队列尾部读出数据(以下简称pop)。当队列为空,消费者就稍息(稍事休息);当队列满(达到最大长度),生产者就稍息。整个流程并不复杂。
优点:逻辑清晰、代码简单、维护方便。
缺点:在每次push时,可能涉及到堆内存的分配;在每次pop时,可能涉及堆内存的释放。假如⽣生产者和消费者都很勤快,频繁地push、pop,那内存分配的开销就很可观了。
环形缓冲区
环形缓冲区与队列缓冲区的外部接口基本一致,这里就不再细说了,主要介绍环形缓冲区的内部机制。这里用一个形象的例子解说:将生产者写入(R)与消费者读出(R)想象成在田径场跑道上赛跑的两个人(R追逐W)。当R追上W的时候,就是缓冲区为空;当W追上R的时候(W比R多跑圈),就是缓冲区满。
环形缓冲区有数组和链表两种方式,数组可以在初始化时将存储空间一次性分配好,不过需要在数组尾部操作时应该注意下一个操作的数据应回到数组的开头;链表不需要头尾相连的特殊处理,但是在初始化的时候是比较繁琐的。
下面先介绍基于队列(单链表实现)的生产者-消费者模型测试用例!
函数介绍
int pthread_mutex_lock(pthread_mutex_t *mutex);//加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);//尝试加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);//解锁
等待
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);//time式
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);//阻塞时
唤醒
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
具体实现
1)编写Makefile
2)编写list_proc.c
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//初始化锁
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//初始化条件变量
typedef struct Node
{
int _data;
struct Node *_next;
}node_t, *node_p, **node_pp;//定义节点结构
node_p head;//头结点
static node_p alloc_node(int data)//开辟一个节点的空间
{
node_p tmp = (node_p)malloc(sizeof(node_t));
if(!tmp)
{
perror("malloc");
exit(1);
}
tmp->_data = data;
tmp->_next = NULL;
}
void Init_List(node_pp l)//初始化链表
{
*l = alloc_node(0);
}
void Push_Front(node_p l, int data)//头插(生产者生成数据)
{
node_p tmp = alloc_node(data);
tmp->_next = l->_next;
l->_next = tmp;
}
int is_Empty(node_p l)//判空
{
return l->_next == NULL ? 1 : 0;
}
void Pop_Front(node_p l, int *outdata)//头删(消费者拿走数据)
{
if(!is_Empty(l))
{
node_p h = l->_next;
l->_next = h->_next;
*outdata = h->_data;
free(h);
h = NULL;
}
}
void Destroy_List(node_p l)
{
int outdata;
while(!is_Empty(l))
{
Pop_Front(l, &outdata);
}
free(l);
l = NULL;
}
void Show_List(node_p l)
{
node_p start = l->_next;
while(start)
{
printf("%d ", start->_data);
start = start->_next;
}
printf("\n");
}
void *productor(void *arg)//生产者
{
while(1)
{
int data = rand()%123;
pthread_mutex_lock(&lock);
Push_Front(head, data);
pthread_mutex_unlock(&lock);
pthread_cond_signal(&cond);
printf("The product success: %d\n", data);
sleep(2);
}
}
void consumer(void *arg)//消费者
{
while(1)
{
int data = -1;
pthread_mutex_lock(&lock);
while(is_Empty(head))
{
printf("No data! The consume waiting...\n");
pthread_cond_wait(&cond, &lock);
}
Pop_Front(head, &data);
pthread_mutex_unlock(&lock);
printf("The consume success: %d\n", data);
}
}
int main()
{
pthread_t product;
pthread_t consume;
pthread_create(&product, NULL, productor, NULL);
pthread_create(&consume, NULL, consumer, NULL);
Init_List(&head);
Destroy_List(head);
pthread_join(product, NULL);
pthread_join(consume, NULL);
return 0;
}
3)测试结果
下面是基于环形队列(数组实现)的生产者-消费者模型的测试用例!
注:消费者永远在生产者之后;
生产者不能超过消费者一圈。
这里为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,保证在任一时刻只能有一个执行线程访问代码的临界区域,我们引入信号量机制来协调进程对共享资源的访问。
信号量的工作原理
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行;
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。
具体实现
1)ring_sem.c
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<semaphore.h>
#define SIZE 6
int arr[SIZE] = {0};
int ret = 0;
sem_t datasem;
sem_t blanksem;
void *productor(void *arg)
{
int i = 0;
while(1)
{
sem_wait(&blanksem);
arr[ret] = i;
printf("productor done! %d\n",arr[ret]);
sem_post(&datasem);
i++;
ret++;
ret %= SIZE;
}
}
void *consumer(void *arg)
{
while(1)
{
sem_wait(&datasem);
printf("consumer done! %d\n",arr[ret]);
sem_post(&blanksem);
sleep(1);
}
}
int main()
{
pthread_t product, consume;
sem_init(&datasem, 0, 0);
sem_init(&blanksem, 0, SIZE);
pthread_create(&product, NULL, productor, NULL);
pthread_create(&consume, NULL, consumer, NULL);
pthread_join(product, NULL);
pthread_join(consume, NULL);
sem_destroy(&datasem);
sem_destroy(&blanksem);
return 0;
}