电子科大操作系统:实验一生产者消费者问题

一、参考内容

1、参考资料

https://blog.csdn.net/aiqq136/article/details/124224796

2、前提知识

在这里插入图片描述

1、POSIX线程函数

头文件:#include<pthread.h>
数据类型:pthread_t; 线程ID
线程相关函数:
1、返回调用线程的ID:pthread_t pthread_self();
2、创建线程:int pthread_create(pthread_t *tidp, const pthread_attr_t attr, void *(start_rtn)(void), void *arg);
解释参数
tidp:指向线程ID的指针,当函数成功返回时将存储所创建的子线程ID
attr:用于定制各种不同的线程属性(一般直接传入空指针NULL,采用默认线程属性)
start_rtn:线程的启动例程,即新创建的线程从该函数开始执行。该函数只有一个参数,即arg
arg:作为start_rtn的第一个参数
返回值:成功返回0,出错时返回各种错误码

3、线程退出:void pthread_exit(void *rval_ptr);
rval_ptr指向线程返回值。
线程退出有以下几种情况:

  1. 线程从例程函数返回。
  2. 线程被其它线程取消。
  3. 线程调用pthread_exit()主动退出。
    4、等待一个线程结束:int pthread_join(pthread_t thread, void **rval_ptr);
  1. thread:要等待的线程的标识符。这个线程必须是由 pthread_create 创建的线程。
  2. rval_ptr:这是一个可选的参数,如果线程需要返回一个值,那么这个返回值会被存储在这个指针指向的内存地址中。如果不需要线程返回值,可以将这个参数设置为
    NULL。 rval_ptr参数用于获取线程返回值。 函数的作用是:
  • 阻塞(调用 pthread_join 的线程),直到 thread 参数指定的线程结束。 想要把哪个线程阻塞,就调用pthread_join函数,在这个函数里传递参数thread,就是阻塞那个线程,然后让这个thread线程进行—>在一个线程里调用另一个线程,等这个线程执行完了再执行
  • 如果 rval_ptr 不是 NULL,那么被等待的线程如果有返回值,这个返回值会被存储在 rval_ptr 指向的内存地址中。 返回值:
  • 成功时,pthread_join 返回0。
  • 出错时,返回相应的错误码。 使用 pthread_join 的一个典型场景是当主线程创建了一个或多个工作线程去执行某些任务,然后需要等待这些工作线程完成它们的任务后再继续执行。这样可以确保所有线程协调地运行,并且资源被正确地清理和释放。
    返回值:成功返回0,错误返回错误编号

2、POSIX中互斥量

头文件:#include <pthread.h>
数据类型:
pthread_mutex_t //互斥量
pthread_mutexattr_t //互斥量的属性
互斥量相关的函数:
对一个互斥量进行初始化:int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
销毁一个互斥量:int pthread_mutex_destroy(pthread_mutex_t *mutex);
返回值:成功则返回0,否则返回错误编号

互斥量加锁:int pthread_mutex_lock(pthread_mutex_t *mutex);

参数解释:

  • mutex:指向 pthread_mutex_t 类型的互斥量对象的指针。这个互斥量对象必须已经被初始化。 函数的作用是:
  • 如果互斥量当前没有被其他线程锁定,pthread_mutex_lock 将锁定互斥量,并且调用线程将继续执行。
  • 如果互斥量已经被其他线程锁定,调用 pthread_mutex_lock 的线程将被阻塞,直到互斥量被解锁。 返回值:
  • 成功时,返回 0。
  • 出错时,返回相应的错误码。 互斥量的使用通常遵循以下模式:
  1. 初始化互斥量。
  2. 当线程需要访问受保护的共享资源时,先锁定互斥量。
  3. 访问共享资源。
  4. 完成访问后,解锁互斥量。

互斥量非阻塞加锁:int pthread_mutex_trylock(pthread_mutex_t *mutex);
互斥量解锁:int pthread_mutex_unlock(pthread_mutex_t *mutex);
互斥量示例
pthread_mutex_t mutex; // 互斥信号量, 一次只有一个线程访问缓冲
pthread_mutex_init(&mutex, NULL);//1.加锁,保证互斥的访问缓冲区
pthread_mutex_lock(&mutex);//2.处理缓冲区的代码
//3.解锁
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);

3、信号量

头文件:#include<semaphore.h>
信号量数据类型:sem_t
主要函数:
sem_init(sem_t *sem, int pshared, unsigned int value);//初始化一个无名信号量
sem_destroy(sem_t *sem);//销毁一个无名信号量
返回值:成功返回 0;错误返回 -1,并设置errno 。
sem_post(sem_t *sem);//信号量值加1。若有线程阻塞于信号量sem,则调度器会唤醒对应阻塞队列中的某一个线程。
sem_wait(sem_t *sem);//若sem小于0,则线程阻塞于信号量sem,直到sem大于0。否则信号量值减1。
sem_trywait(sem_t *sem);//功能同sem_wait(),但此函数不阻塞,若sem小于0,直接返回。
返回值:成功返回0,错误返回-1,并设置errno 。
信号量示例
sem_t sem;
sem_init(&sem, 0, 1);//初始化一个值为1的信号量
sem_wait(&sem);//获取信号量,相当于P操作
//do somthing
sem_post(&sem);//释放信号量,相当于V操作
sem_destroy(&sem);//销毁一个无名信号量

二、分析过程

解释作业PPT内容

1、有一群生产者任务在生产产品,并将这些产品提供给消费者任务去消费。为使生产者任务与消费者进程能并发执行,在两者之间设置了一个具有n个缓冲区的缓冲池:

解释:缓冲池可以是一个数据结构

struct{
         mutex;//一个互斥信号量  
        char buffer[100]; //这样是只有一个缓冲区
        或者
        
        char buffer[5][100];//这个是有5个缓冲区 
        }

生产者任务从文件中读取一个数据,并将它存放到一个缓冲区中

解释:这里的文件是需要提前准备一个文本文件txt内容任意,可以是字符或数字
这是一个producer()函数,

消费者任务从一个缓冲区中取走数据,并输出此数据(printf)
生产者和消费者之间必须保持同步原则:不允许消费者任务到一个空缓冲区去取产品;也不允许生产者任务向一个已装满产品且尚未被取走的缓冲区中投放产品。
2、创建3个进程(或者线程)作为生产者任务,4个进程(或者线程)作为消费者任务
3、创建一个文件作为数据源,文件中事先写入一些内容作为数据(字符串或者数值) 4、生产者和消费者任务(进程或者线程)都具有相同的优先级

在这里插入图片描述

使用两个信号量,一个代表空缓冲区的数量,一个代表已经写入数据的缓冲区数量!!

需要有信号量full_sem和empty_sem

三、代码实现

1、提前准备:

  1. 库函数准备:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>   // 实现多线程的头文件
#include <semaphore.h> // 实现信号量定义的头文件
  1. 定义缓冲区并初始化缓冲区
char buffer[5][5]={};  //设置5个缓冲区,每次读5个字符;
  1. 申请同步信号量和互斥信号量
sem_t empty_sem; // 信号量的数据类型为结构sem_t,它本质上是一个长整型的数,同步信号量, 当满了时阻止生产者放产品
sem_t full_sem;   // 同步信号量, 当没产品时阻止消费者消费
pthread_mutex_t mutex[5]; // 互斥信号量, 一次只有一个线程访问缓冲,因为有5个缓冲区,所以申请5个互斥信号量
  1. 定义生产者和消费者线程ID
pthread_t id_producer[3]; // 声明生产者线程的ID数组,3个
pthread_t id_consumer[4]; // 声明消费者线程的ID数组,4个
  1. 初始化信号量
// 初始化同步信号量
int ini1 = sem_init(&empty_sem, 0, M); 
int ini2 = sem_init(&full_sem, 0, 0); // 同上初始化的描述
 
//初始化互斥信号量的函数
int ini3 = pthread_mutex_init(&mutex, NULL); 
  1. 额外的需要读取文件,需要提前准备进行文件操作的内容
FILE *file; // 文件指针
char filename[] = "file.txt"; // 替换为你的文件名
char line[1024]; // 用于存储从文件中读取的每一行

2、生产者方法:

整体过程:(整体是一个循环)

  1. 申请空的信号量:sem_wait(&empty_sem);
    这个函数解决两种情况:1、full_sem<=0,缓冲区没有任何东西,线程就会堵塞,同时full_sem -1。2、full_sem>0,full_sem直接-1。
sem_wait(&sem_empty);
  1. 缓冲区加锁实现互斥:pthread_mutex_lock(&mutex);
    这个函数解决两种情况:1、mutex已经被其他线程锁定,这个函数就锁定当前的线程2、mutex未被其他线程锁定,这个函数就锁定这个mutex。
pthread_mutex_lock(&mutex[in]);

因为有5个缓冲区,是不是应该先判断一下这个要投放的缓冲区是不是空闲的???但是和只有1个缓冲区的情况怎么解释?????可以先按一个缓冲区来。
3. 使用缓冲区:具体要做的事情

  int in=0;
   in = in % 5;   //投放的位置
   
   // 打开文件
    file = fopen(source.txt, "r"); // "r" 表示以只读方式打开文件
    if (file == NULL) {
        perror("Error opening file"); // 如果文件无法打开,打印错误信息
        return EXIT_FAILURE;
    }
   fread(butter[in], sizeof(char), 5, file); // 读取5个字符
   // 关闭文件
   fclose(file);
   in++;
  1. 解锁缓冲区: pthread_mutex_unlock(&mutex);
pthread_mutex_unlock(&mutex[in]);

都要使用到in的话,应该等解锁完了再in++;
5. 释放空/满信号量:sem_post(&empty_sem);

sem_post(&sem_full);  

3、消费者方法

  1. 申请同步信号量full,就是看看有没有剩的商品
sem_wait(&full_sem);
  1. 缓冲区加锁
pthread_mutex_lock(&mutex[out]);
  1. 使用缓冲区
   int out =0;
   out = out % 5;
   printf("%c",buff[out]);
   buffer[out] = 0;  //缓冲区清空
   out++;
  1. 缓冲区解锁
pthread_mutex_unlock(&mutex[out]);

同样这里要使用out,是不是应该之后再out++;

  1. 释放信号量
sem_post(&empty_sem);

4、主函数

在这里插入图片描述

  1. 先定义一些会用到的
int ret_producer[3];
int ret_consumer[4];
  1. 初始化同步信号量和互斥信号量
    // 初始化同步信号量
int ini1 = sem_init(&empty_sem, 0, M); 
int ini2 = sem_init(&full_sem, 0, 0); // 同上初始化的描述
 
//初始化互斥信号量的函数
int ini3 = pthread_mutex_init(&mutex, NULL); 

为什么要赋值????????
3. 创建生产者和消费者线程

// 创建3个生产者线程
for(i = 0; i < 3; i++)
{
   ret1[i] = pthread_create(&id_producer[i], NULL, product, NULL);
}
//创建4个消费者线程
for(i = 0; i < 4; i++)
{
   ret2[i] = pthread_create(&id_consumer[i], NULL, prochase, NULL);
}
  1. 等待任务执行结束
//销毁线程
for(i = 0; i < producerNumber; i++)
{
   pthread_join(id1[i],NULL);
   //pthread_join()函数来使主线程阻塞以等待其他线程退出
}
 
for(i = 0; i < consumerNumber; i++)
{
  pthread_join(id2[i],NULL);
}
 
exit(0);

四、最终代码

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

char buffer[5] = {0};    //关于字符串数组的初始化

//申请信号量
sem_t sem_empty;
sem_t sem_full;
pthread_mutex_t mutex;

//定义线程标识符
pthread_t id_producer[3];
pthread_t id_consumer[4];

FILE *file;
char filename[] = "source.txt";


//生产者方法,注意:开关文件不在循环里,若开关文件一直循环,则只会读取第一个字符,这个函数里只有一个判断终止的情况,来关文件
void *producer(void *arg) {
    while (1) {
        sleep(1);    //速度太快,需要慢一点,这个速度刚刚好
        sem_wait(&sem_empty);
        pthread_mutex_lock(&mutex);
        
        int in=0;
        char c = fgetc(file);      //从文件中读取一个字符并复制给c
        if (c == EOF) {             //文件末尾的情况
            printf("Error reading file or file EOF reached\n");
            fclose(file); 
            file = NULL;
            pthread_mutex_unlock(&mutex);
            sem_post(&sem_full);
            return NULL; 
        } else {
            buffer[in % 5] = c;    //一定要有这个中间变量
        }

        pthread_mutex_unlock(&mutex);
        sem_post(&sem_full);
    }
}

void *consumer(void *arg) {
    int id = 0; 
    while (1) {
        sleep(1);
        sem_wait(&sem_full);
        pthread_mutex_lock(&mutex);
        
        int out=0;
        out = out % 5; 
        printf("Consumer %ld obtained character: %c\n", (long)pthread_self(), buffer[out]);
        buffer[out] = '\0';      //把缓冲区清空

        pthread_mutex_unlock(&mutex);
        sem_post(&sem_empty);
    }
}

int main() {
    int ret_producer[3];
    int ret_consumer[4];
    int i;

    //初始化信号量,这个必须在主函数里完成
    if (sem_init(&sem_empty, 0, 5) < 0) {
        perror("sem_init failed for sem_empty");
        exit(EXIT_FAILURE);
    }
    if (sem_init(&sem_full, 0, 0) < 0) {
        perror("sem_init failed for sem_full");
        exit(EXIT_FAILURE);
    }
    if (pthread_mutex_init(&mutex, NULL) != 0) {
        perror("mutex init has failed");
        exit(EXIT_FAILURE);
    }
    //打开文件,要在主函数里执行
    file = fopen(filename, "r"); 
    if (file == NULL) {
        perror("Error opening file");
        exit(EXIT_FAILURE);
    }

   //创建生产者和消费者线程
    for (i = 0; i < 3; i++) {
        ret_producer[i] = pthread_create(&id_producer[i], NULL, producer, NULL);
    }
    for (i = 0; i < 4; i++) {
        ret_consumer[i] = pthread_create(&id_consumer[i], NULL, consumer, NULL);
    }

    //等待线程结束
    for (i = 0; i < 3; i++) {
        pthread_join(id_producer[i], NULL);
    }
    for (i = 0; i < 4; i++) {
        pthread_join(id_consumer[i], NULL);
    }
    //关闭文件
    if (file != NULL) {
        fclose(file);
    }
    //销毁信号量
    sem_destroy(&sem_empty);
    sem_destroy(&sem_full);
    pthread_mutex_destroy(&mutex);

    exit(0);
}

五、遇到的问题和知识点

其他内容

  1. 文件的使用方法
FILE *file;    //定义一个文件指针
char filename[] = "source.txt";   //注意这是访问同级文件,若在更高级的目录或者子集目录,用法不一样

char c = fgetc(file); //从文件中获取一个字符   为什么可以 

//主函数中的
 file = fopen(filename, "r");   //
    if (file == NULL) {        //必要的判断终止的情况
        perror("Error opening file");
        exit(EXIT_FAILURE);
    }
    
//producer中的
if (c == EOF) {     //必要的读取最后一个结束字符的终止情况
            printf("Error reading file or file EOF reached\n");
            fclose(file); 
            file = NULL;
            pthread_mutex_unlock(&mutex);
            sem_post(&sem_full);
            return NULL; 
        } else {
            buffer[in % 5] = c; 
        }
  1. 我们的目的是打开文件,让fgetc一直循环一直读,直到读到最后一个字符。所以需要文件一直开着,文件不能开关循环,所以开文件卸载主函数中。
  2. sleep(1); //速度太快,需要慢一点,这个速度刚刚好
  3. char c = fgetc(file);
    函数原型:int fgetc(FILE *stream);
    返回值:
  • fgetc() 返回读取的字符,其类型为 int。如果成功读取了字符,返回的是字符的整数表示。若需要字符,可以提前给变量设置好类型。
  • 如果到达文件末尾(EOF),返回 EOF(通常在 <stdio.h> 头文件中定义)。
  • 如果发生错误,也返回 EOF,可以通过 ferror(stream) 函数检查是否发生错误。
    需要处理出现错误和文件末尾的情况
   int ch; // 使用 int 类型变量接收返回值
    while ((ch = fgetc(file)) != EOF) { // 读取字符直到文件结束
        putchar(ch); // 打印读取的字符
    }

    if (ferror(file)) { // 检查是否发生错误
        perror("Error reading file");
    }

用到的信号量和线程函数解释

  1. sem_wait(sem_t *sem);//若sem小于0,则线程阻塞于信号量sem,直到sem大于0。否则信号量值减1。
    sem_wait(&full_sem);//在消费者方法中,若full_sem<0,即缓冲区没有任何东西,则线程阻塞
  2. sem_post(sem_t *sem);//信号量值加1。若有线程阻塞于信号量sem,则调度器会唤醒对应阻塞队列中的某一个线程。
  3. pthread_mutex_lock(pthread_mutex_t *mutex);
    pthread_mutex_lock(&mutex); 这个函数解决两种情况:1、mutex已经被其他线程锁定,这个函数就锁定当前的线程2、mutex未被其他线程锁定,这个函数就锁定这个mutex。值得注意的是,这个信号量需要被初始化。

互斥量的使用通常遵循以下模式:

  1. 初始化互斥量。
  2. 当线程需要访问受保护的共享资源时,先锁定互斥量。
  3. 访问共享资源。
  4. 完成访问后,解锁互斥量。
  1. sem_init(&sem, 0, 1);//初始化一个值为1的信号量
  2. pthread_t 数据类型

在 C 语言中,使用 pthread_t 类型定义了一个数组来存储线程标识符。pthread_t 是 POSIX
线程库(pthreads)中定义的一个数据类型,用于唯一标识一个线程。 以下是您的代码片段的解释: pthread_t
id_producer[3]; 这行代码声明了一个名为 id_producer 的数组,它可以存储 3 个 pthread_t
类型的元素。每个元素将用来存储由 pthread_create 函数创建的每个生产者线程的线程标识符。

pthread_t id_consumer[4];

同样,这行代码声明了一个名为 id_consumer 的数组,它可以存储 4 个 pthread_t 类型的元素。每个元素将存储由
pthread_create 创建的每个消费者线程的线程标识符。

为什么要有这一步?

  1. 标识线程:每个线程都需要一个唯一的标识符,这样你就可以区分不同的线程,特别是在进行线程同步或需要获取特定线程的状态时。

  2. 同步和通信:线程标识符用于各种 pthreads 函数中,如 pthread_join、pthread_detach 或 pthread_cancel,这些函数可能需要知道它们操作的确切线程。

  3. 错误处理:如果 pthread_create 调用失败,它会返回一个错误代码,而不是一个有效的线程标识符。通过检查数组中的值,你可以确定线程是否成功创建。

  4. 等待线程结束:在程序结束时,你可能需要等待所有线程完成它们的任务。使用 pthread_join 函数,你可以等待特定线程的结束,确保程序优雅地退出。

  5. 资源清理:在线程结束运行后,可能需要执行一些清理工作,如释放动态分配的资源。通过线程标识符,你可以跟踪哪些线程已经结束,并执行必要的清理。

在多线程程序中,管理线程标识符是确保线程正确同步和资源得到适当管理的重要部分。

  1. pthread_self()返回当前执行线程的线程标识符(pthread_t 类型)
  2. 初始化信号量的方法
  if (sem_init(&sem_empty, 0, 5) < 0) {
        perror("sem_init failed for sem_empty");
        exit(EXIT_FAILURE);
    }
    if (sem_init(&sem_full, 0, 0) < 0) {
        perror("sem_init failed for sem_full");
        exit(EXIT_FAILURE);
    }
    if (pthread_mutex_init(&mutex, NULL) != 0) {
        perror("mutex init has failed");
        exit(EXIT_FAILURE);
    }
  1. 创建线程,创建线程的同时会使用线程方法
  for (i = 0; i < 3; i++) {
        ret_producer[i] = pthread_create(&id_producer[i], NULL, producer, NULL);
    }
    for (i = 0; i < 4; i++) {
        ret_consumer[i] = pthread_create(&id_consumer[i], NULL, consumer, NULL);
    }
  1. 等待线程结束(必要的一步)
    //等待线程结束
  for (i = 0; i < 3; i++) {
        pthread_join(id_producer[i], NULL);
    }
    for (i = 0; i < 4; i++) {
        pthread_join(id_consumer[i], NULL);
    }
  1. 销毁线程
sem_destroy(&sem_empty);
sem_destroy(&sem_full);
pthread_mutex_destroy(&mutex);
  • 13
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
(1)创建生产者消费者线程 在Windows2000环境下,创建一个控制台进程,在此进程中创建n个线程来模拟生产者或者消费者。这些线程的信息由本程序定义的“测试用例文件”中予以指定。 该文件的格式和含义如下: 3 1 P 3 2 P 4 3 C 4 1 4 P 2 5 C 3 1 2 4 第一行说明程序中设置几个临界区,其余每行分别描述了一个生产者或者消费者线程的信息。每一行的各字段间用Tab键隔开。不管是消费者还是生产者,都有一个对应的线程号,即每一行开始字段那个整数。第二个字段用字母P或者C区分是生产者还是消费者。第三个字段表示在进入相应线程后,在进行生产和消费动作前的休眠时间,以秒计时;这样做的目的是可以通过调整这一列参数,控制开始进行生产和消费动作的时间。如果是代表生产者,则该行只有三个字段。如果代表消费者,则该行后边还有若干字段,代表要求消费的产品所对应的生产者的线程号。所以务必确认这些对应的线程号存在并且该线程代表一个生产者。 (2)生产和消费的规则 在按照上述要求创建线程进行相应的读写操作时,还需要符合以下要求: ①共享缓冲区存在空闲空间时,生产者即可使用共享缓冲区。 ②从上边的测试数据文件例子可以看出,某一生产者生产一个产品后,可能不止一个消费者,或者一个消费者多次地请求消费该产品。此时,只有当所有的消费需求都被满足以后,该产品所在的共享缓冲区才可以被释放,并作为空闲空间允许新的生产者使用。 ③每个消费者线程的各个消费需求之间存在先后顺序。例如上述测试用例文件包含一行信息“5 C 3 l 2 4”,可知这代表一个消费者线程,该线程请求消费1,2,4号生产者线程生产的产品。而这种消费是有严格顺序的,消费1号线程产品的请求得到满足后才能继续往下请求2号生产者线程的产品。 ④要求在每个线程发出读写操作申请、开始读写操作和结束读写操作时分别显示提示信息。 (3)相关基础知识 本实验所使用的生产者消费者模型具有如下特点: 本实验的多个缓冲区不是环形循环的,也不要求按顺序访问。生产者可以把产品放到目前某一个空缓冲区中。 消费者只消费指定生产者的产品。 在测试用例文件中指定了所有的生产和消费的需求,只有当共享缓冲区的数据满足了所有关于它的消费需求后,此共享缓冲区才可以作为空闲空间允许新的生产者使用。 本实验在为生产者分配缓冲区时各生产者间必须互斥,此后各个生产者的具体生产活动可以并发。而消费者之间只有在对同一产品进行消费时才需要互斥,同时它们在消费过程结束时需要判断该消费对象是否已经消费完毕并清除该产品。 Windows用来实现同步和互斥的实体。在Windows中,常见的同步对象有:信号量(Semaphore)、互斥量(Mutex)、临界段(CriticalSection)等。使用这些对象都分为三个步骤,一是创建或者初始化:接着请求该同步对象,随即进入临界区,这一步对应于互斥量的上锁;最后释放该同步对象,这对应于互斥量的解锁。这些同步对象在一个线程中创建,在其他线程中都可以使用,从而实现同步互斥。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值