操作系统-第六次实验

实验5 生产者-消费者问题

一、实验内容

 模拟操作系统中进程同步和互斥。

 在Linux操作系统下用C实现经典同步问题:生产者—消费者,具体要求如下:
 (1)一个大小为10的缓冲区,初始状态为空。
 (2)2个生产者,随机等待一段时间,往缓冲区中添加数据,若缓冲区已满,等待消费者取走数据之后再添加,重复10次。
 (3)2个消费者,随机等待一段时间,从缓冲区中读取数据,若缓冲区为空,等待生产者添加数据之后再读取,重复10次。

二、实验目的

 • 熟悉临界资源、信号量及PV操作的定义与物理意义
 • 了解进程通信的方法
 • 掌握进程互斥与进程同步的相关知识
 • 掌握用信号量机制解决进程之间的同步与互斥问题
 • 实现生产者-消费者问题,深刻理解进程同步问题

三、实验原理

 本实验的主要目的是模拟操作系统中进程同步和互斥。在系统进程并发执行异步推进的过程中,由于资源共享和进程间合作而造成进程间相互制约。进程间的相互制约有两种不同的方式。
 (1)间接制约。这是由于多个进程共享同一资源(如CPU、共享输入/输出设备)而引起的,即共享资源的多个进程因系统协调使用资源而相互制约。
 (2)直接制约。只是由于进程合作中各个进程为完成同一任务而造成的,即并发进程各自的执行结果互为对方的执行条件,从而限制各个进程的执行速度。

 生产者和消费者是经典的进程同步问题,在这个问题中,生产者不断的向缓冲区中写入数据,而消费者则从缓冲区中读取数据。生产者进程和消费者对缓冲区的操作是互斥,即当前只能有一个进程对这个缓冲区进行操作,生产者进入操作缓冲区之前,先要看缓冲区是否已满,如果缓冲区已满,则它必须等待消费者进程将数据取出才能写入数据,同样的,消费者进程从缓冲区读取数据之前,也要判断缓冲区是否为空,如果为空,则必须等待生产者进程写入数据才能读取数据。

 在本实验中,进程之间要进行通信来操作同一缓冲区。一般来说,进程间的通信根据通信内容可以划分为两种:即控制信息的传送与大批量数据传送。有时,也把进程间控制信息的交换称为低级通信,而把进程间大批量数据的交换称为高级通信。
 目前,计算机系统中用得比较普遍的高级通信机制可分为3大类:共享存储器系统、消息传递系统及管道通信系统。

 • 共享存储器系统
 共享存储器系统为了传送大量数据,在存储器中划出一块共享存储区,诸进程可通过对共享存储区进行读数据或写数据以实现通信。进程在通信之前,向系统申请共享存储区中的一个分区,并为它指定一个分区关键字。

 • 消息传递系统
 在消息传递系统中,进程间的数据交换以消息为单位,在计算机网络中被称为报文。消息传递系统的实现方式又可以分为以下两种:
 (1)直接通信方式
 发送进程可将消息直接发送给接收进程,即将消息挂在接收进程的消息缓冲队列上,而接收进程可从自己的消息缓冲队列中取得消息。
 (2)间接通信方式
 发送进程将消息发送到指定的信箱中,而接收进程从信箱中取得消息。这种通信方式又称信箱通信方式,被广泛地应用于计算机网络中。相应地,该消息传递系统被称为电子邮件系统。

 • 管道通信系统
 向管道提供输入的发送进程,以字符流方式将大量的数据送入管道,而接收进程从管道中接收数据。由于发送进程和接收进程是利用管道进行通信的,故称为管道通信。为了协调发送和接收双方的通信,管道通信机制必须提供以下3方面的协调功能。
 (1)互斥
 当一个进程正在对pipe文件进行读或写操作时,另一个进程必须等待。
 (2)同步
 当写进程把一定数量的数据写入pipe文件后,便阻塞等待,直到读进程取走数据后,再把写进程唤醒。
 (3)确认对方是否存在
 只有确定对方已存在时,才能进行管道通信,否则会造成因对方不存在而无限制地等待。

四、实验内容

1.算法设计

【算法流程】
 在这个问题当中,我们采用信号量机制进行进程之间的通信,设置两个信号量,空的信号量和满的信号量。在Linux系统中,一个或多个信号量构成一个信号量集合。使用信号量机制可以实现进程之间的同步和互斥,允许并发进程一次对一组信号量进行相同或不同的操作。每个P、V操作不限于减1或加1,而是可以加减任何整数。在进程终止时,系统可根据需要自动消除所有被进程操作过的信号量的影响。
 1.缓冲区采用循环队列表示,利用头、尾指针in、out来存放、读取数据,以及判断队列是否为空。缓冲区中数组大小为10;
 2.利用随机函数rand()得到A~Z的一个随机字符,作为生产者每次生产的数据,存放到缓冲区中;
 3.使用pthread_create()函数创建创建2个生产者线程以及2个消费者线程,每个进程有自己的PID号和作为生产者和消费者的标识。
 4.采用pthread_cond_t建立信号量,使用pthread_cond_wait()函数和pthread_cond_signal()函数实现线程间的PV操作。
 5.采用pthread_mutex_t建立互斥锁,使用pthread_mutex_lock()函数和pthread_mutex_lock()函数对互斥锁进行操作。
 6.一个线程的一次操作完成后,采用函数Sleep()函数使线程随机睡眠。
 7.最后使用pthread_join()函数释放线程。

【主程序流程图】

【生产者进程流程图】

【消费者进程流程图】

【生产者-消费者问题伪代码设计】

producer() {
    while( true ) {
       wait( full );  // 等待缓冲区有空闲位置,在使用PV操作时,条件变量需要在互斥锁之前
        wait( mutex );  // 保证在product时不会有其他线程访问缓冲区
        // 生产者操作
        buf.push( item, out );  // 将新资源放到buf[out]位置
       out = ( out + 1 ) % buf.size();
        signal( empty );  // 通知consumer缓冲区有资源可以取走
        signal( mutex );  // 解开互斥锁
    }
}
consumer() {
    while( true ) {
        wait( empty );  // 等待缓冲区有资源可以使用,在使用PV操作时,条件变量需要在互斥锁之前
        wait mutex );  // 保证在consume时不会有其他线程访问缓冲区
        // 消费者操作
        buf.pop( in );  // 将buf[in]位置的的资源取走
        in = ( in + 1 ) % buf.size();
        signal( full);  // 通知缓冲区有空闲位置
        signal( mutex );  // 解开互斥锁
    }
}

2.源代码

#include <stdio.h>
#include <pthread.h>
#define buffersize 10
int Time = 0; //记录执行时间

//信号量设置
pthread_cond_t full = PTHREAD_COND_INITIALIZER;        //缓存区满的变量
pthread_cond_t empty = PTHREAD_COND_INITIALIZER;       //缓存区空的变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;      //互斥锁

char buf[buffersize] = { ' ',' ',' ',' ',' ',' ',' ',' ',' ',' ' };     //缓冲区
int num = 0;                //缓冲区中产品的数量
int in = 0, out = 0;     //头尾指针

//随机产生缓冲区中的字母
char get_char()
{
    return ('A' + rand() % 26);
}

//生产者
void* producer(void* args)
{
    int* x = (int*)args; //记录生产者为1还是为2
    int sum = 0;
    while (sum < 10)
    {
       pthread_mutex_lock(&mutex);    //加互斥锁
       //判断缓冲区是否为满
       while (num == buffersize) {
           pthread_cond_wait(&full, &mutex);  //满的话,生产者要在full这个条件变量上阻塞
       }
       //生产产品
       char c = get_char();
       buf[out] = c;     //生产的产品放最后面
       out = (out + 1) % buffersize;
       num++;
       sum++;
       Time++;
       //打印缓冲区中的数据
       printf("   %d  \t ", Time);
       int i;
       if (in < out) {
           for (i = in; i <= out; i++)
              printf("%c ", buf[i]);
           for (i = out + 1; i < buffersize; i++)
              printf("  ");
           for (i = 0; i < in; i++)
              printf("  ");
       }
       else {
           for (i = in; i < buffersize; i++)
              printf("%c ", buf[i]);
           for (i = 0; i <= out; i++)
              printf("%c ", buf[i]);
           for (i = out + 1; i < in; i++)
              printf("  ");
       }
       printf("\t生产者%d\t\t生产%c\n", *x, c);
       pthread_cond_signal(&empty);   //生产了一个产品就可以叫消费者来取
       pthread_mutex_unlock(&mutex);  //解开互斥锁
       Sleep(rand() % 500);        //随机睡眠
    }
}

//消费者
void* consumer(void* args)
{
    int* x = (int*)args; //记录消费者为1还是为2
    int sum = 0;
    while (sum < 10)
    {
       pthread_mutex_lock(&mutex);    //加互斥锁
       //判断缓冲区是否为空
       while (num == 0) {
           pthread_cond_wait(&empty, &mutex); //若为空,消费者阻塞在条件变量empty上
       }
       //对缓冲区进行操作
       char c = buf[in];
       buf[in] = ' ';
       in = (in + 1) % buffersize;
       num--;
       sum++;
       Time++;
       //打印缓冲区中的数据
       printf("   %d  \t ", Time);
       int i;
       if (in < out) {
           for (i = in; i <= out; i++)
              printf("%c ", buf[i]);
           for (i = out + 1; i < buffersize; i++)
              printf("  ");
           for (i = 0; i < in; i++)
              printf("  ");
       }
       else {
           for (i = in; i < buffersize; i++)
              printf("%c ", buf[i]);
           for (i = 0; i <= out; i++)
              printf("%c ", buf[i]);
           for (i = out + 1; i < in; i++)
              printf("  ");
       }
       printf("\t消费者%d\t\t消费%c\n", *x, c);
       pthread_cond_signal(&full);    //消费者消费掉一个产品,就可以唤醒生产者了
       pthread_mutex_unlock(&mutex);  //解开互斥锁
       Sleep(rand() % 500);        //随机睡眠
    }
}

int main(int argc, char* argv[])
{
    pthread_t pid[4]; //四个线程
    int t1 = 1;
    int t2 = 2;

    //创建线程
    printf("执行时间 缓存区数据\t     当前执行的进程  生产或消费的数据\n");
    pthread_create(&pid[0], NULL, producer, (void*)&t1);
    pthread_create(&pid[1], NULL, producer, (void*)&t2);
    pthread_create(&pid[2], NULL, consumer, (void*)&t1);
    pthread_create(&pid[3], NULL, consumer, (void*)&t2);

    //回收线程
    void* result;
    //等待线程结束并回收资源,线程之间同步
    int i;
    for (i = 0; i < 4; i++) {
       if (pthread_join(pid[i], &result) == -1) {
           printf("进程回收失败!\n"); //进程回收,如果回收失败输出错误提示
           exit(1);                //结束线程
       }
    }
    printf("进程回收成功!\n");
    return 0;
}

3.编译结果

五、思考题

以上是通过进程来实现生产者和消费者问题,考虑如何采用线程来实现。

 答:使用#include <pthread.h>头文件下的pthread_create()函数创建线程,并在线程结束后利用pthread_join()函数回收资源。

六、实验总结

1.互斥锁与条件变量的使用比较

 我们会发现,在伪代码中强调了条件变量在前,互斥锁在后,而到了代码实现时又变成了先加互斥锁,再进行循环pthread_cond_wait()。这是为什么呢?
 其实,在伪代码中的wait()、signal()就是操作系统中的PV操作,而PV操作定义就保证了该语句是原子操作,因此在wait条件变量改变的时候不会因为多进程同时访问共享资源造成混乱,所以为了保证线程间的同步,需要先加条件变量,等事件可使用后才进行线程相应的操作,此时互斥锁的作用是保证共享资源不会被其他线程访问。
 而在代码实现中,signal()对应的时pthread_cond_wait()函数,该函数在执行时会有三步:
 • 解开当前的锁
 • 等待条件变量达到所需要的状态
 • 再把之前解开的锁加锁
 为了实现将pthread_cond_wait()变成原子操作,就需要在该函数之前添加互斥锁。因为pthread_cond_wait()可以解锁,也就不会发生像伪代码所说的死锁问题。相反,如果像伪代码那样先使用条件变量,后加锁,则会造成多个线程同时访问共享资源的问题,造成数据的混乱。

2.心得体会

 通过本次实验,清楚掌握了生产者-消费者问题的实现,对进程间同步问题有了更好的理解,同时接触了#include <pthread.h>头文件下的函数,对线程的创建、执行、回收有了更好的理解,明白了线程间同步与互斥之间的操作关系,更好地掌握了同步这一章节的内容。

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

我是蒸饺吖~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值