生产者消费者模型
概述
生产者消费者模型通过一个缓冲区来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间互不感知,而通过缓冲区来进行通讯,生产者线程产生数据放入缓冲区,消费者线程直接从缓冲区读取数据。在这种模型中,生产者产生的资源不易丢失,两线程通讯的速度不在取决于速度较慢的一方,平衡了生产者和消费者的处理能力。
缓冲区
对缓冲区的临界保护和互斥访问是生产者消费者模型的核心
- 理想缓冲区:永远不满且不空
- 实际缓冲区:满不能放入,空不能提取。
所以缓冲区的设计须考虑时间和空间的平衡性。
常见的缓冲区结构有很多,本例以环形缓冲区为例介绍环形缓冲区的实现及使用
概述
可以把环形缓冲区的读出端R
和写入端W
想象成是两个人在环形体育场跑道上追逐,R追逐W且R不能超过W。当R
追上W
的时候(R和W运动距离相同),就是缓冲区为空;当W
追上R
的时候(W比R多跑一圈),就是缓冲区满。
特点
- 在使用上,环形缓冲区和队列缓冲区没有太大区别,都有写入和读出的接口,缓冲区满和空的状态。
- 在实现上,队列缓冲区在写入和读出时会申请和释放内存,环形缓冲区则在固定的内存上进行,写入时不会申请内存,而是直接覆盖掉旧的数据,读出时也不会释放内存。所以环形方式相比队列方式,无需对于缓冲区分配、释放,这是环形缓冲区的一个主要优势。
实现
环形缓冲区的实现有两种方式,数组或链表。数组需要在逻辑上实现首尾相接的闭环,而链表初始化比较麻烦,在不使用时须手动释放,本文以数组实现为例,讲解环形缓冲区的实现。
#define BUFFER_LEN 20 //缓冲区长度
typedef struct {
char str[5];
int value;
} Data, *pData; //缓冲区数据结构
static int g_iRead = 0; //读索引
static int g_iWrite = 0; //写索引
static Data g_tData[BUFFER_LEN]; //缓冲区
/* 缓冲区是否满 */
static int isBufferFull(void)
{
return (g_iRead == ((g_iWrite + 1) % BUFFER_LEN));
}
/* 缓冲区是否空 */
static int isBufferEmpty(void)
{
return (g_iRead == g_iWrite);
}
/* 将数据放入缓冲区 */
static int PutToBuffer(pData ptData)
{
if (!isBufferFull()) {
g_tData[g_iWrite] = *ptData;
g_iWrite = (g_iWrite + 1) % BUFFER_LEN;
return 1;
} else {
return 0;
}
}
/* 从缓冲区读取数据 */
static int GetFromBuffer(pData ptData)
{
if (!isBufferEmpty()) {
*ptData = g_tData[g_iRead];
g_iRead = (g_iRead + 1) % BUFFER_LEN;
return 1;
} else {
return 0;
}
}
-
环形缓冲区须实现四个基本函数,判断空,判断满,读数据,写数据。在使用过程中不直接操作数组,而是使用这四个函数完成相关操作。
-
在判断空和满时读索引和写索引逻辑上都指向同一个位置,这样就无法正确区分是空是满。可以选择牺牲掉一个存储空间,当读索引和写索引指向同一位置时为空,写索引差一个空间就要覆盖读索引所指的空间时即为满。
-
在写入时须确保缓冲区没满,写入后移动写索引。在读取时须确保缓冲区非空,读取后移动读索引。在实现读写索引的移动时,需要使用
%
的方式,这样在达到数组最大长度时可以返回到数组首地址。
线程逻辑
生产者线程:
获得缓冲区锁
if (缓冲区满)
睡入非满条件变量,释放缓冲区锁,等待被唤醒
生产资源->非空条件变量
唤醒在非空条件变量中睡眠的消费者线程
释放缓冲区锁
消费者线程:
获得缓冲区锁
if (缓冲区空)
睡入非空条件变量,释放缓冲区锁,等待被唤醒
消费资源->非满条件变量
唤醒在非满条件变量中睡眠的生产者线程
释放缓冲区锁
-
生产者线程和消费者线程是相互被唤醒的逻辑。
-
只有一个线程能够对缓冲区进行读写操作,形成互斥访问。
-
当线程睡眠时要释放缓冲区锁,避免其他线程满足条件却获取不到缓冲区锁
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#define BUFFER_LEN 20
typedef struct {
char str[5];
int value;
} Data, *pData;
static int g_iRead = 0;
static int g_iWrite = 0;
static Data g_tData[BUFFER_LEN];
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; // 互斥锁
pthread_cond_t g_full = PTHREAD_COND_INITIALIZER; // 非满条件变量
pthread_cond_t g_empty = PTHREAD_COND_INITIALIZER; // 非空条件变量
static int isBufferFull(void)
{
return (g_iRead == ((g_iWrite + 1) % BUFFER_LEN));
}
static int isBufferEmpty(void)
{
return (g_iRead == g_iWrite);
}
static int PutToBuffer(pData ptData)
{
if (!isBufferFull()) {
g_tData[g_iWrite] = *ptData;
g_iWrite = (g_iWrite + 1) % BUFFER_LEN;
return 1;
} else {
return 0;
}
}
static int GetFromBuffer(pData ptData)
{
if (!isBufferEmpty()) {
*ptData = g_tData[g_iRead];
g_iRead = (g_iRead + 1) % BUFFER_LEN;
return 1;
} else {
return 0;
}
}
/* 显示缓冲区状态 */
void show(char const *who, char const *op, pData ptData)
{
/* 打印调用此函数线程名 */
printf("%s:", who);
/* 打印缓冲区数据 */
for (int i = 0; i < g_iWrite - g_iRead; i++) {
printf("(%s%d) ", g_tData[g_iRead + i].str, g_tData[g_iRead + i].value);
}
/* 打印缓冲区操作 */
printf("%s(%s%d)\n", op, ptData->str, ptData->value);
}
/* 生产者线程 */
void *producer(void *arg)
{
Data tData;
char buff[5];
char const *who = (char const *)arg;
for (;;) {
pthread_mutex_lock(&g_mutex);
if (isBufferFull()) {
printf("\033[;;32m%s:满仓!\033[0m\n", who);
pthread_cond_wait(&g_full, &g_mutex);
}
sprintf(buff, "%c:", (char)(g_iWrite + 65));
memcpy(tData.str, buff, sizeof(buff));
tData.value = g_iWrite;
show(who, " <- ", &tData);
PutToBuffer(&tData);
pthread_cond_signal(&g_empty);
pthread_mutex_unlock(&g_mutex);
usleep((rand() % 100) * 1000);
}
return NULL;
}
/* 消费者线程 */
void *customer(void *arg)
{
Data tData;
char const *who = (char const *)arg;
for (;;) {
pthread_mutex_lock(&g_mutex);
if (isBufferEmpty()) {
printf("\033[;;31m%s:空仓!\033[0m\n", who);
pthread_cond_wait(&g_empty, &g_mutex);
}
GetFromBuffer(&tData);
show(who, " -> ", &tData);
pthread_cond_signal(&g_full);
pthread_mutex_unlock(&g_mutex);
usleep((rand() % 100) * 1000);
}
return NULL;
}
int main(void)
{
srand(time(NULL));
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_t tid;
pthread_create(&tid, &attr, producer, "生产者1");
pthread_create(&tid, &attr, producer, "生产者2");
pthread_create(&tid, &attr, customer, "消费者1");
pthread_create(&tid, &attr, customer, "消费者2");
getchar();
return 0;
}
- 在主函数中初始化随机数,模拟资源生产消费的不确定性。创建了两个生产者线程,两个消费者线程,之后主函数阻塞在
getchar()
函数中。 - 生产者线程获取缓冲区锁,生产
Data
类型的数据,唤醒消费者线程,释放缓冲区锁 - 消费者线程获取缓冲区锁,读取
Data
类型的数据,唤醒生产者线程,释放缓冲区锁
执行结果
生产者1: <- (A:0)
消费者2: -> (A:0)
消费者1:空仓!
生产者2: <- (B:1)
消费者1: -> (B:1)
生产者1: <- (C:2)
生产者1:(C:2) <- (D:3)
消费者2:(D:3) -> (C:2)
消费者2: -> (D:3)
生产者1: <- (E:4)
消费者1: -> (E:4)
消费者2:空仓!
生产者2: <- (F:5)
消费者2: -> (F:5)
生产者1: <- (G:6)
生产者1:(G:6) <- (H:7)
消费者2:(H:7) -> (G:6)
消费者1: -> (H:7)
消费者2:空仓!
生产者2: <- (I:8)
消费者2: -> (I:8)
生产者1: <- (J:9)
消费者1: -> (J:9)
......