目录
一、参考内容
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指向线程返回值。
线程退出有以下几种情况:
- 线程从例程函数返回。
- 线程被其它线程取消。
- 线程调用pthread_exit()主动退出。
4、等待一个线程结束:int pthread_join(pthread_t thread, void **rval_ptr);
- thread:要等待的线程的标识符。这个线程必须是由 pthread_create 创建的线程。
- 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。
- 出错时,返回相应的错误码。 互斥量的使用通常遵循以下模式:
- 初始化互斥量。
- 当线程需要访问受保护的共享资源时,先锁定互斥量。
- 访问共享资源。
- 完成访问后,解锁互斥量。
互斥量非阻塞加锁: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、提前准备:
- 库函数准备:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h> // 实现多线程的头文件
#include <semaphore.h> // 实现信号量定义的头文件
- 定义缓冲区并初始化缓冲区
char buffer[5][5]={}; //设置5个缓冲区,每次读5个字符;
- 申请同步信号量和互斥信号量
sem_t empty_sem; // 信号量的数据类型为结构sem_t,它本质上是一个长整型的数,同步信号量, 当满了时阻止生产者放产品
sem_t full_sem; // 同步信号量, 当没产品时阻止消费者消费
pthread_mutex_t mutex[5]; // 互斥信号量, 一次只有一个线程访问缓冲,因为有5个缓冲区,所以申请5个互斥信号量
- 定义生产者和消费者线程ID
pthread_t id_producer[3]; // 声明生产者线程的ID数组,3个
pthread_t id_consumer[4]; // 声明消费者线程的ID数组,4个
- 初始化信号量
// 初始化同步信号量
int ini1 = sem_init(&empty_sem, 0, M);
int ini2 = sem_init(&full_sem, 0, 0); // 同上初始化的描述
//初始化互斥信号量的函数
int ini3 = pthread_mutex_init(&mutex, NULL);
- 额外的需要读取文件,需要提前准备进行文件操作的内容
FILE *file; // 文件指针
char filename[] = "file.txt"; // 替换为你的文件名
char line[1024]; // 用于存储从文件中读取的每一行
2、生产者方法:
整体过程:(整体是一个循环)
- 申请空的信号量:
sem_wait(&empty_sem);
这个函数解决两种情况:1、full_sem<=0,缓冲区没有任何东西,线程就会堵塞,同时full_sem -1。2、full_sem>0,full_sem直接-1。
sem_wait(&sem_empty);
- 缓冲区加锁实现互斥:
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++;
- 解锁缓冲区:
pthread_mutex_unlock(&mutex);
pthread_mutex_unlock(&mutex[in]);
都要使用到in的话,应该等解锁完了再in++;
5. 释放空/满信号量:sem_post(&empty_sem);
sem_post(&sem_full);
3、消费者方法
- 申请同步信号量full,就是看看有没有剩的商品
sem_wait(&full_sem);
- 缓冲区加锁
pthread_mutex_lock(&mutex[out]);
- 使用缓冲区
int out =0;
out = out % 5;
printf("%c",buff[out]);
buffer[out] = 0; //缓冲区清空
out++;
- 缓冲区解锁
pthread_mutex_unlock(&mutex[out]);
同样这里要使用out,是不是应该之后再out++;
- 释放信号量
sem_post(&empty_sem);
4、主函数
- 先定义一些会用到的
int ret_producer[3];
int ret_consumer[4];
- 初始化同步信号量和互斥信号量
// 初始化同步信号量
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);
}
- 等待任务执行结束
//销毁线程
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);
}
五、遇到的问题和知识点
其他内容
- 文件的使用方法
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;
}
- 我们的目的是打开文件,让fgetc一直循环一直读,直到读到最后一个字符。所以需要文件一直开着,文件不能开关循环,所以开文件卸载主函数中。
- sleep(1); //速度太快,需要慢一点,这个速度刚刚好
- 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");
}
用到的信号量和线程函数解释
sem_wait(sem_t *sem);
//若sem小于0,则线程阻塞于信号量sem,直到sem大于0。否则信号量值减1。
sem_wait(&full_sem);//在消费者方法中,若full_sem<0,即缓冲区没有任何东西,则线程阻塞sem_post(sem_t *sem);
//信号量值加1。若有线程阻塞于信号量sem,则调度器会唤醒对应阻塞队列中的某一个线程。pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_lock(&mutex); 这个函数解决两种情况:1、mutex已经被其他线程锁定,这个函数就锁定当前的线程2、mutex未被其他线程锁定,这个函数就锁定这个mutex。值得注意的是,这个信号量需要被初始化。
互斥量的使用通常遵循以下模式:
- 初始化互斥量。
- 当线程需要访问受保护的共享资源时,先锁定互斥量。
- 访问共享资源。
- 完成访问后,解锁互斥量。
- sem_init(&sem, 0, 1);//初始化一个值为1的信号量
- 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 创建的每个消费者线程的线程标识符。为什么要有这一步?
标识线程:每个线程都需要一个唯一的标识符,这样你就可以区分不同的线程,特别是在进行线程同步或需要获取特定线程的状态时。
同步和通信:线程标识符用于各种 pthreads 函数中,如 pthread_join、pthread_detach 或 pthread_cancel,这些函数可能需要知道它们操作的确切线程。
错误处理:如果 pthread_create 调用失败,它会返回一个错误代码,而不是一个有效的线程标识符。通过检查数组中的值,你可以确定线程是否成功创建。
等待线程结束:在程序结束时,你可能需要等待所有线程完成它们的任务。使用 pthread_join 函数,你可以等待特定线程的结束,确保程序优雅地退出。
资源清理:在线程结束运行后,可能需要执行一些清理工作,如释放动态分配的资源。通过线程标识符,你可以跟踪哪些线程已经结束,并执行必要的清理。
在多线程程序中,管理线程标识符是确保线程正确同步和资源得到适当管理的重要部分。
- pthread_self()返回当前执行线程的线程标识符(pthread_t 类型)
- 初始化信号量的方法
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);
}
- 创建线程,创建线程的同时会使用线程方法
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);
}
- 销毁线程
sem_destroy(&sem_empty);
sem_destroy(&sem_full);
pthread_mutex_destroy(&mutex);