经典生产者-消费者问题解析

69 篇文章 13 订阅
12 篇文章 0 订阅

1.生产者-消费者问题

生产者和消费者问题在现实系统中是很普遍的。例如在一个多媒体系统中,生产者编码视频帧,而消费者消费(解码)视频帧,缓冲区的目的就是减少视频流的抖动。又如在图形用户接口设计中,生产者检测到鼠标和键盘事件,并将其插入到缓冲区中。消费者以某种基于优先级的方式从缓冲区中取出这些事件并显示在屏幕上。

生产者和消费者模式共享一个有n个槽位的有限缓冲区。生产者反复地生成新的item,并将它插入到缓冲区中。消费者不断从缓冲区中取出这些item,并消费它们。它有3个显著特点:

  • 因为生产和消费都涉及共享变量的更新,所以必须保证对缓冲区的访问是互斥的。
  • 如果缓冲区是满的,那么生产者必须等待直到有一个槽位可用。
  • 如果缓冲区是空的,那么消费者必须等待直到有一个item可以。

“生产者—消费者”问题描述如下:有一个有限缓冲区和两个线程(生产者和消费者),他们分别不停地把产品放入缓冲区和从缓冲区中拿走产 品。一个生产者在缓冲区满的时候必须等待,一个消费者在缓冲区空的时候也必须等待。另外,因为缓冲区是临界资源,所以生产者和消费者之间必须互斥执行。这里要求使用有名管道来模拟有限缓冲区,并且使用信号量来解决“生产者—消费者”问题中的同步和互斥问题。

这里使用 3 个信号量,其中两个信号量 avail 和 full 分别用于解决生产者和消费者线程之间的同步问题,mutex 是 用于这两个线程之间的互斥问题。其中 avail 表示有界缓冲区中的空单元数,初始值为 N;full 表示有界缓冲区中 非空单元数,初始值为 0;mutex 是互斥信号量,初始值为 1。流程图如下所示:

image-20230211222502658

本实验的代码中采用的有界缓冲区拥有 3 个单元,每个单元为 5 个字节。为了尽量体现每个信号量 的意义,在程序中生产过程和消费过程是随机(采取 0~5s 的随机时间间隔)进行的,而且生产者的 速度比消费者的速度平均快两倍左右(这种关系可以相反)。生产者一次生产一个单元的产品(放入 “hello”字符串),消费者一次消费一个单元的产品。

/*************************************************************************
	> File Name: pthread_Producer_customer.c
	> Author:	 leon
	> Mail: 	 liangzc1124@163.com
	> Created Time: Sat 11 Feb 2023 10:27:48 PM CST
 ************************************************************************/

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h>
#include <unistd.h> 
#include <fcntl.h> 
#include <pthread.h> 
#include <errno.h> 
#include <semaphore.h> 
#include <sys/ipc.h> 
#include <sys/types.h> 
#include <sys/stat.h> 

#define MYFIFO "myfifo" /* 缓冲区有名管道的名字 */ 
#define BUFFER_SIZE 3 /* 缓冲区的单元数 */ 
#define UNIT_SIZE 5 /* 每个单元的大小 */ 
#define RUN_TIME 30 /* 运行时间 */ 
#define DELAY_TIME_LEVELS 5.0 /* 周期的最大值 */ 

int fd; 
time_t end_time; 
sem_t mutex, full, avail; /* 3 个信号量 */

/*生产者线程*/ 
void *producer(void *arg) 
{ 
    int real_write;
    int delay_time = 0;

    while(time(NULL) < end_time)
    {
        delay_time = (int)(rand() * DELAY_TIME_LEVELS/(RAND_MAX) / 2.0) + 1;
        sleep(delay_time);
        
        /*P 操作信号量 avail 和 mutex*/
        sem_wait(&avail);
        sem_wait(&mutex);
        printf("\nProducer: delay = %d\n", delay_time);

        /*生产者写入数据*/
        if ((real_write = write(fd, "hello", UNIT_SIZE)) == -1)
        {
            if(errno == EAGAIN) 
            { 
                printf("The FIFO has not been read yet.Please try later\n"); 
            }
        }
        else
        {
            printf("Write %d to the FIFO\n", real_write);
        }

        /*V 操作信号量 full 和 mutex*/ 
        sem_post(&full); 
        sem_post(&mutex);
    }
    pthread_exit(NULL);
}



/* 消费者线程*/ 
void *customer(void *arg) 
{ 
    unsigned char read_buffer[UNIT_SIZE]; 
    int real_read; 
    int delay_time; 
 
    while(time(NULL) < end_time) 
    { 
        delay_time = (int)(rand() * DELAY_TIME_LEVELS/(RAND_MAX)) + 1;
        sleep(delay_time);

        /*P 操作信号量 full 和 mutex*/ 
        sem_wait(&full); 
        sem_wait(&mutex); 
        memset(read_buffer, 0, UNIT_SIZE); 
        printf("\nCustomer: delay = %d\n", delay_time); 

        if ((real_read = read(fd, read_buffer, UNIT_SIZE)) == -1) 
        { 
            if (errno == EAGAIN) 
            { 
                printf("No data yet\n"); 
            } 
        } 
        printf("Read %s from FIFO\n", read_buffer); 
 
        /*V 操作信号量 avail 和 mutex*/ 
        sem_post(&avail); 
        sem_post(&mutex);
    } 
    pthread_exit(NULL); 
}


int main() 
{ 
    pthread_t thrd_prd_id,thrd_cst_id; 
    pthread_t mon_th_id; 
    int ret; 
 
    srand(time(NULL)); 
    end_time = time(NULL) + RUN_TIME; 
    
    /*创建有名管道*/ 
    if((mkfifo(MYFIFO, 0644 | O_CREAT | O_EXCL) < 0) && (errno != EEXIST)) 
    { 
        printf("Cannot create fifo\n"); 
        return errno; 
    } 
 
    /*打开管道*/ 
    fd = open(MYFIFO, O_RDWR); 
    if (fd == -1) 
    { 
        printf("Open fifo error\n"); 
        return fd; 
    } 
 
    /*初始化互斥信号量为 1*/ 
    ret = sem_init(&mutex, 0, 1); 
    /*初始化 avail 信号量为 N*/ 
    ret += sem_init(&avail, 0, BUFFER_SIZE); 
    /*初始化 full 信号量为 0*/ 
    ret += sem_init(&full, 0, 0); 
    if (ret != 0) 
    { 
        printf("Any semaphore initialization failed\n"); 
        return ret; 
    }

    /*创建两个线程*/ 
    ret = pthread_create(&thrd_prd_id, NULL, producer, NULL); 
    if (ret != 0) 
    { 
        printf("Create producer thread error\n"); 
        return ret; 
    } 
    ret = pthread_create(&thrd_cst_id, NULL, customer, NULL); 
    if(ret != 0) 
    { 
        printf("Create customer thread error\n"); 
        return ret; 
    } 
    pthread_join(thrd_prd_id, NULL); 
    pthread_join(thrd_cst_id, NULL); 
    close(fd); 
    unlink(MYFIFO);
    return 0; 
} 
  • 运行结果如下:
leon@ubuntu:~/letcode/pthread$ ./pthread_prod_cust 

Producer: delay = 2
Write 5 to the FIFO

Customer: delay = 3
Read hello from FIFO

...

Producer: delay = 3
Write 5 to the FIFO

Producer: delay = 1
Write 5 to the FIFO

Customer: delay = 4
Read hello from FIFO

Producer: delay = 2
Write 5 to the FIFO

Producer: delay = 2
Write 5 to the FIFO

Customer: delay = 3
Read hello from FIFO

Producer: delay = 3
Write 5 to the FIFO

Customer: delay = 5
Read hello from FIFO
leon@ubuntu:~/letcode/pthread$ 

2. 读者-写者问题

读者-写者问题在现实系统中也比较常见。例如,一个在线影院座位预定系统中,允许有无限多个客户同时查看(读者)座位分配,但是正在预定的客户必须拥有对数据库的独占访问(写者)。读者写者问题又分为以下几种情况:

  • 读者优先,即除非有写者正在写,否则不能让读者等;
  • 写者优先,即只要写者准备好写,就尽快完成写。在写者发出写请求后到达的读者,必须等待;

下文给出读者优先的一个示例:

/*全局变量*/
int readcnt;	//统计当前在临界区中读者的数量
sem_t mutex;	//保护对readcnt的访问
sem_t w;		//控制对访问共享对象的临界区的访问

void reader(void)
{
    while(1){
        P(&mutex);
        readcnt++;
        if(readcnt == 1)	//first in
            P(&w);
        V(&mutex);
        
        /* 临界区操作语句 */
        /* 读语句 */
        
        P(&mutex);
        readcnt--;
        if(readcnt == 0)	//last out
            V(&w);
        V(&mutex);
    }
}

void writer(void)
{
    while(1){
        P(&w);
        
        /* 临界区操作语句 */
        /* 写语句 */
        
        V(&w);
    }
}
  • 为了保证任意时刻临界区中只有一个写者,每当一个写者进入临界区时,它对互斥锁w加锁,每当它离开临界区时,对w解锁;

  • 为了保证只要还有一个读者占用互斥锁w,那么无限多的读者就可以无障碍的进入临界区读,只有第一个进入临界区的读者对w加锁,只有最后一个离开的读者对w解锁。

    此法可能导致饥饿(starvation):如果有读者不断到达,写者就无限期等待。

    3. 基于预线程化的并发服务器

    前文叙述了如何使用信号量来访问共享变量和调度对共享资源的访问,现在可以动手实现一个基于预线程化的技术(prethreading)的并发服务器开发。

如图所示,服务器是由一个主线程和一组工作者线程构成的。主线程不断接受来自客户端的连接请求,并将得到的连接描述符放在一个缓冲区中。每一个工作者线程反复地从共享缓冲区中取出描述符为客户端服务,然后等待下一个描述符。

下面给出具体代码:

#include "csapp.h"
#include "sbuf.h"
#define NTHREADS  4
#define SBUFSIZE  16

void echo_cnt(int connfd);
void *thread(void *vargp);

sbuf_t sbuf; /* Shared buffer of connected descriptors */

int main(int argc, char **argv) 
{
    int i, listenfd, connfd;
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;
    pthread_t tid; 

    if (argc != 2) {
	fprintf(stderr, "usage: %s <port>\n", argv[0]);
	exit(0);
    }
    listenfd = Open_listenfd(argv[1]);

    sbuf_init(&sbuf, SBUFSIZE); //line:conc:pre:initsbuf
    for (i = 0; i < NTHREADS; i++)  /* Create worker threads */ 
	Pthread_create(&tid, NULL, thread, NULL);               
    while (1) { 
        clientlen = sizeof(struct sockaddr_storage);
        connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen);
        sbuf_insert(&sbuf, connfd); /* Insert connfd in buffer */
    }
}

void *thread(void *vargp) 
{  
    Pthread_detach(pthread_self()); 
    while (1) { 
	int connfd = sbuf_remove(&sbuf); /* Remove connfd from buffer */ 
	echo_cnt(connfd);                /* Service client */
	Close(connfd);
    }
}
  • 首先初始化缓冲区sbuf(line 24)后,主线程创建了一组工作者线程(line 25~26)。
  • 之后进入无限循环,接受连接请求,并将得到的已连接描述符插入缓冲区sbuf中。
  • 每个工作者线程的行为非常简单,它等待直到能从缓冲区中取出一个已连接描述符(line 39),然后调用echo_cnt函数回送客户端的输入。

下面给出echo_cnt函数的代码,它向你展示了一个从线程例程调用的初始化程序包的一般技术。其中全局变量byte_cnt中记录了从所有客户端接受到的累计字节数。

#define RIO_BUFSIZE 8192
typedef struct {
    int rio_fd;                /* Descriptor for this internal buf */
    int rio_cnt;               /* Unread bytes in internal buf */
    char *rio_bufptr;          /* Next unread byte in internal buf */
    char rio_buf[RIO_BUFSIZE]; /* Internal buffer */
} rio_t;

static int byte_cnt;  /* Byte counter */
static sem_t mutex;   /* and the mutex that protects it */

static void init_echo_cnt(void)
{
    Sem_init(&mutex, 0, 1);
    byte_cnt = 0;
}

void echo_cnt(int connfd) 
{
    int n; 
    char buf[MAXLINE]; 
    rio_t rio;
    static pthread_once_t once = PTHREAD_ONCE_INIT;

    Pthread_once(&once, init_echo_cnt); 
    Rio_readinitb(&rio, connfd);        
    while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
        P(&mutex);
        byte_cnt += n; //line:conc:pre:cntaccess1
        printf("server received %d (%d total) bytes on fd %d\n", 
               n, byte_cnt, connfd); 
        V(&mutex);
        Rio_writen(connfd, buf, n);
    }
}

void rio_readinitb(rio_t *rp, int fd) 
{
    rp->rio_fd = fd;  
    rp->rio_cnt = 0;  
    rp->rio_bufptr = rp->rio_buf;
}

ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen) 
{
    int n, rc;
    char c, *bufp = usrbuf;
    for (n = 1; n < maxlen; n++) { 
        if ((rc = rio_read(rp, &c, 1)) == 1) {
            *bufp++ = c;
            if (c == '\n') {
                n++;
                break;
            }
        } else if (rc == 0) {
	    	if (n == 1)
				return 0; /* EOF, no data read */
	   		else
				break;    /* EOF, some data was read */
		} else
	    	return -1;	  /* Error */
    }
    *bufp = 0;
    return n-1;
}

ssize_t rio_writen(int fd, void *usrbuf, size_t n) 
{
    size_t nleft = n;
    ssize_t nwritten;
 	char *bufp = usrbuf;
    while (nleft > 0) {
        if ((nwritten = write(fd, bufp, nleft)) <= 0) {
            if (errno == EINTR)  /* Interrupted by sig handler return */
                nwritten = 0;    /* and call write() again */
            else
                return -1;       /* errno set by write() */
        }
        nleft -= nwritten;
        bufp += nwritten;
    }
    return n;
}
  • 首先初始化byte_cnt计数器和mutex信号量;
  • 一种是显式地调用一个初始化函数,一种是上文所采取的利用pthread_once函数。即当第一次有某个线程调用echo_cnt函数时,使用pthread_once函数去调用初始化函数。这个方法的优点是使程序包的使用更加容易,缺点使每一次调用echo_cnt函数都会导致调用pthread_once函数,而除了第一次,它没有做什么有用的事。
    首先初始化byte_cnt计数器和mutex信号量;
  • 一种是显式地调用一个初始化函数,一种是上文所采取的利用pthread_once函数。即当第一次有某个线程调用echo_cnt函数时,使用pthread_once函数去调用初始化函数。这个方法的优点是使程序包的使用更加容易,缺点使每一次调用echo_cnt函数都会导致调用pthread_once函数,而除了第一次,它没有做什么有用的事。
  • 一旦程序包被初始化,echo_cnt函数会初始化RIO带缓冲区的I/O包(line 20),然后回送从客户端接收到的每一个文本行。

获取更多知识,请点击关注:
嵌入式Linux&ARM
CSDN博客
简书博客
知乎专栏


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leon_George

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值