一、学习内容
-
线程的同步互斥机制
-
同步机制之条件变量
-
概念
1> 条件变量实现的是一个生产者对多个消费者问题
2> 条件变量本质上维护了一个队列,所有消费者线程想要执行之前先要进入该队列中。等待生产者线程来唤醒。先进入等待队列中的线程被先唤醒。由于,对于消费者而言,这个等待队列相当于是一个临界资源,当两个线程同时想要进入等待队列时,会产生竞态,此时我们需要引入互斥锁来解决。 -
创建条件变量
pthread_cond_t cond ; -
初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);-
返回值
成功返回0,失败返回非0错误码 -
参数
参数1: 条件变量的地址
参数2:条件变量的属性,一般填NULL
-
功能
初始化条件变量
-
-
消费者线程进入阻塞队列
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);-
返回值
成功返回0,失败返回非0错误码 -
参数
参数1:条件变量的地址
参数2:互斥锁地址
-
功能
进入等待队列
-
-
唤醒消费者线程
int pthread_cond_signal(pthread_cond_t *cond);-
返回值
成功返回0,失败返回非0错误码 -
参数
条件变量的地址 -
功能
唤醒所有等待队列中的消费者线程
-
-
唤醒消费者线程
int pthread_cond_signal(pthread_cond_t *cond);-
返回值
成功返回0,失败返回非0错误码 -
参数
条件变量的地址 -
功能
唤醒在队头的消费者线程
-
-
销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);-
返回值
成功返回0,失败返回非0错误码 -
参数
条件变量的地址 -
功能
销毁条件变量
-
-
-
-
进程之间的通信(nterprocess communication)
-
概念
-
1>可以使用一个进程向文件中写入数据,另一个进程从文件中读取数据。但是,进程的执行没有先后顺序,对于这种方法,必须要保证写进程先执行,读进程后执行,需要保证进程的同步
2>多个进程之间用户空间相互独立,但是内核空间是共享,我们可以通过内核空间进行数据的传递
-
-
分类
-
内核提供的通信方式
-
无名管道
-
有名管道
-
信号
-
-
system V提供的通信方式
-
消息队列
-
共享内存
-
信号量集
-
-
套接字通信(socket)
跨主机的多进程间的通信
-
-
无名管道
-
概念
1> 顾名思义就是没有名字的管道文件
2> 是一种特殊的文件,会在内存中创建出来,并且返回该文件的两个文件描述符,分别对应该文件的读端和写端
3> 由于该管道文件没有名字,故,不能用于非亲缘进程间的通信,只能用于亲缘进程间的通信
4> 由于管道文件返回的是文件描述符,所以,只能使用文件IO相关函数进行操作
5> 对于管道文件的操作是一次性的,当写入管道中的数据,被某个进程读取后,数据就不存在了
6> 由于是一次性的操作,所以对于管道文件的操作是不能使用lseek进行光标偏移的
7> 管道通信方式属于半双工的通信方式
1、单工:任意时刻,都只能是A向B发送数据,B不能向A发送数据
2、半双工:同一时刻,只能是A向B发送数据,或者是B向A发送数据3、全双工:任意时刻,AB可以互相通信
-
API
int pipe(int pipefd[2]);-
返回值
成功返回0,失败返回-1并置位错误码 -
参数
用于接收创建的管道文件的文件描述符,pipefd[0]表示管道的读端,pipefd[1]表示管道的写端 -
功能
在内核空间创建一个管道文件,并返回该文件的两端的文件描述符
-
-
特点
1、管道文件的大小:64KB
2、无名管道适用于亲缘进程间通信,也适用于自己跟自己通信
-
读写特点
-
当读管道存在时
写管道有多少写多少,写满64K后在write处阻塞 -
当读管道不存在时
写管道管道破裂,内核空间会向用户空间发送一个SIGPIPE信号,进程收到该信号后,默认杀死进程 -
当写管道存在时
读管道有多少读多少,没有数据。会在read处阻塞 -
当写管道不存在时
读管道有多少读多少,没有数据,也不会在read处阻塞
-
-
-
有名管道
-
概念
1> 顾名思义就是有名字的管道通信,在通信过程中,会在文件系统上创建一个实实在在的管道文件(p)
2> 有名管道既可以实现亲缘进程间通信,也可以实现非亲缘进程间的通信
3> 虽然会在文件系统中创建一个管道文件,但是该文件只用于通信使用,不用于数据的存储
4> 对于有名管道文件的操作也是一次性的,写入到管道中的文件数据被读取后就不存在了
5> 当某个进程,以只写的形式打开文件时,如果没有一个进程以只读的形式打开该文件,那么上一个进程就会在open处阻塞 反之亦然。
6> 有名管道的读写特点跟无名管道的读写特点保持一致
-
API
int mkfifo(const char *pathname, mode_t mode);-
返回值
功返回0, 失败返回-1并置位错误码 -
参数
-
参数1:要创建的管道文件的文件名
-
参数2:管道文件的权限
-
-
功能
在文件系统中创建一个有名管道文件
-
-
-
-
脑图
二、作业
作业1:
按照图中实例完成使用有名管道实现两个进程之间的相互通信
代码解答:
方法一:进程法
文件create.c(创建管道文件)
#include <myhead.h> // 包含自定义的头文件
int main(int argc, const char *argv[])
{
// 创建名为 "fifoAtoB" 的有名管道,权限为0664(可读写)
if (mkfifo("./fifoAtoB", 0664) == -1)
{
// 如果创建失败,输出错误信息并返回 -1
perror("mkfifo error for fifoAtoB");
return -1;
}
// 创建名为 "fifoBtoA" 的有名管道,权限为0664
if (mkfifo("./fifoBtoA", 0664) == -1)
{
// 如果创建失败,输出错误信息并返回 -1
perror("mkfifo error for fifoBtoA");
return -1;
}
getchar(); // 等待用户输入,暂停程序以防管道立即被删除
// 删除 "fifoAtoB" 管道文件
unlink("fifoAtoB");
// 删除 "fifoBtoA" 管道文件
unlink("fifoBtoA");
return 0; // 正常退出程序
}
文件snd.c(进程A)
#include <myhead.h>
int main(int argc, const char *argv[])
{
// 创建子进程
pid_t pid = fork();
// 如果是父进程,执行发送操作
if (pid > 0)
{
// 打开管道文件 "fifoAtoB",以只写方式
int wfd = open("./fifoAtoB", O_WRONLY);
if (wfd == -1)
{
// 打开失败,打印错误信息
perror("open error for fifoAtoB");
return -1; // 返回错误码 -1
}
char wbuf[128] = ""; // 定义写缓冲区
while (1)
{
// 提示用户输入并从标准输入读取字符串
printf("A进程父进程,请输入>>>");
fgets(wbuf, sizeof(wbuf), stdin);
wbuf[strlen(wbuf) - 1] = '\0'; // 去掉末尾的换行符
// 将用户输入写入管道
write(wfd, wbuf, strlen(wbuf));
printf("A进程父进程发送成功\n");
// 如果输入为 "quit",则退出循环
if (strcmp(wbuf, "quit") == 0)
{
break;
}
}
// 关闭管道文件描述符
close(wfd);
}
// 如果是子进程,执行接收操作
else if (pid == 0)
{
// 打开管道文件 "fifoBtoA",以只读方式
int rfd = open("./fifoBtoA", O_RDONLY);
if (rfd == -1)
{
// 打开失败,打印错误信息
perror("open error for fifoBtoA");
return -1; // 返回错误码 -1
}
char rbuf[128] = ""; // 定义读缓冲区
while (1)
{
// 清空缓冲区并从管道读取数据
bzero(rbuf, sizeof(rbuf));
read(rfd, rbuf, sizeof(rbuf));
// 打印收到的数据
printf("A进程子进程收到消息为:%s\n", rbuf);
// 如果收到的数据为 "quit",则退出循环
if (strcmp(rbuf, "quit") == 0)
{
break;
}
}
// 关闭管道文件描述符
close(rfd);
}
// 如果创建子进程失败
else
{
// 打印错误信息
perror("fork error");
return -1; // 返回错误码 -1
}
return 0; // 正常退出
}
文件recv.c(进程B)
#include <myhead.h> // 包含自定义头文件
#include <sys/types.h> // 包含系统类型头文件
#include <unistd.h> // 包含 POSIX 标准头文件
int main(int argc, const char *argv[])
{
// 创建子进程
pid_t pid = fork();
// 如果是父进程,执行接收操作
if (pid > 0)
{
// 打开管道文件 "fifoAtoB",以只读方式
int rfd = open("./fifoAtoB", O_RDONLY);
if (rfd == -1)
{
// 打开失败,打印错误信息
perror("open error for fifoAtoB");
return -1; // 返回错误码 -1
}
char rbuf[128] = ""; // 定义读缓冲区
while (1)
{
// 清空缓冲区并从管道读取数据
bzero(rbuf, sizeof(rbuf));
read(rfd, rbuf, sizeof(rbuf));
// 打印收到的数据
printf("B进程父进程收到消息为:%s\n", rbuf);
// 如果收到的数据为 "quit",则退出循环
if (strcmp(rbuf, "quit") == 0)
{
break;
}
}
// 关闭管道文件描述符
close(rfd);
}
// 如果是子进程,执行发送操作
else if (pid == 0)
{
// 打开管道文件 "fifoBtoA",以只写方式
int wfd = open("./fifoBtoA", O_WRONLY);
if (wfd == -1)
{
// 打开失败,打印错误信息
perror("open error for fifoBtoA");
return -1; // 返回错误码 -1
}
char wbuf[128] = ""; // 定义写缓冲区
while (1)
{
// 提示用户输入并从标准输入读取字符串
printf("B进程子进程,请输入>>>");
fgets(wbuf, sizeof(wbuf), stdin);
wbuf[strlen(wbuf) - 1] = '\0'; // 去掉末尾的换行符
// 将用户输入写入管道
write(wfd, wbuf, strlen(wbuf));
printf("B进程子进程发送成功\n");
// 如果输入为 "quit",则退出循环
if (strcmp(wbuf, "quit") == 0)
{
break;
}
}
// 关闭管道文件描述符
close(wfd);
}
// 如果创建子进程失败
else
{
// 打印错误信息
perror("fork error");
return -1; // 返回错误码 -1
}
return 0; // 正常退出
}
方法二:线程法
文件create.c(创建管道文件)
#include <myhead.h>
int main(int argc, const char *argv[])
{
// 创建一个有名管道文件 fifoAtoB,权限为 0664
if (mkfifo("./fifoAtoB", 0664) == -1)
{
perror("mkfifo error for fifoAtoB"); // 如果创建失败,输出错误信息
return -1; // 返回 -1 表示失败
}
// 创建另一个有名管道文件 fifoBtoA,权限为 0664
if (mkfifo("./fifoBtoA", 0664) == -1)
{
perror("mkfifo error for fifoBtoA"); // 如果创建失败,输出错误信息
return -1; // 返回 -1 表示失败
}
getchar(); // 等待用户输入,暂停程序执行
// 删除创建的有名管道文件 fifoAtoB
unlink("fifoAtoB");
// 删除创建的有名管道文件 fifoBtoA
unlink("fifoBtoA");
return 0; // 返回 0 表示程序成功执行
}
snd.c(进程A)
#include <myhead.h>
// 发送数据到 B 进程的函数
void *send_to_b(void *arg)
{
// 打开管道 fifoAtoB 进行写操作
int wfd = open("./fifoAtoB", O_WRONLY);
if (wfd == -1)
{
perror("open error for fifoAtoB"); // 如果打开失败,输出错误信息
pthread_exit(NULL); // 线程退出
}
char wbuf[128] = ""; // 定义存放数据的缓冲区
while (1)
{
// 提示用户输入数据
printf("A进程父线程,请输入>>> ");
fgets(wbuf, sizeof(wbuf), stdin); // 从标准输入读取数据
wbuf[strlen(wbuf) - 1] = '\0'; // 去掉输入的换行符
// 将数据写入管道
write(wfd, wbuf, strlen(wbuf));
printf("A进程父线程发送成功\n");
// 如果输入 "quit" 则退出循环
if (strcmp(wbuf, "quit") == 0)
{
break;
}
}
close(wfd); // 关闭写管道
pthread_exit(NULL); // 线程退出
}
// 从 B 进程接收数据的函数
void *receive_from_b(void *arg)
{
// 打开管道 fifoBtoA 进行读操作
int rfd = open("./fifoBtoA", O_RDONLY);
if (rfd == -1)
{
perror("open error for fifoBtoA"); // 如果打开失败,输出错误信息
pthread_exit(NULL); // 线程退出
}
char rbuf[128] = ""; // 定义存放数据的缓冲区
while (1)
{
bzero(rbuf, sizeof(rbuf)); // 清空缓冲区
read(rfd, rbuf, sizeof(rbuf)); // 从管道读取数据
printf("A进程子线程收到消息:%s\n", rbuf); // 打印接收到的消息
// 如果接收到 "quit" 则退出循环
if (strcmp(rbuf, "quit") == 0)
{
break;
}
}
close(rfd); // 关闭读管道
pthread_exit(NULL); // 线程退出
}
int main(int argc, const char *argv[])
{
pthread_t tid1, tid2; // 定义两个线程ID
// 创建父线程,负责发送数据到 B 进程
pthread_create(&tid1, NULL, send_to_b, NULL);
// 创建子线程,负责接收来自 B 进程的数据
pthread_create(&tid2, NULL, receive_from_b, NULL);
// 等待两个线程完成
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0; // 程序成功结束
}
recv.c(进程B)
#include <myhead.h>
// 从 A 进程接收数据的函数
void *receive_from_a(void *arg)
{
// 打开管道 fifoAtoB 进行读操作
int rfd = open("./fifoAtoB", O_RDONLY);
if (rfd == -1)
{
perror("open error for fifoAtoB"); // 如果打开失败,输出错误信息
pthread_exit(NULL); // 线程退出
}
char rbuf[128] = ""; // 定义存放数据的缓冲区
while (1)
{
bzero(rbuf, sizeof(rbuf)); // 清空缓冲区
read(rfd, rbuf, sizeof(rbuf)); // 从管道读取数据
printf("B进程父线程收到消息:%s\n", rbuf); // 打印接收到的消息
// 如果接收到 "quit" 则退出循环
if (strcmp(rbuf, "quit") == 0)
{
break;
}
}
close(rfd); // 关闭读管道
pthread_exit(NULL); // 线程退出
}
// 发送数据到 A 进程的函数
void *send_to_a(void *arg)
{
// 打开管道 fifoBtoA 进行写操作
int wfd = open("./fifoBtoA", O_WRONLY);
if (wfd == -1)
{
perror("open error for fifoBtoA"); // 如果打开失败,输出错误信息
pthread_exit(NULL); // 线程退出
}
char wbuf[128] = ""; // 定义存放数据的缓冲区
while (1)
{
// 提示用户输入数据
printf("B进程子线程,请输入>>> ");
fgets(wbuf, sizeof(wbuf), stdin); // 从标准输入读取数据
wbuf[strlen(wbuf) - 1] = '\0'; // 去掉输入的换行符
// 将数据写入管道
write(wfd, wbuf, strlen(wbuf));
printf("B进程子线程发送成功\n");
// 如果输入 "quit" 则退出循环
if (strcmp(wbuf, "quit") == 0)
{
break;
}
}
close(wfd); // 关闭写管道
pthread_exit(NULL); // 线程退出
}
int main(int argc, const char *argv[])
{
pthread_t tid1, tid2; // 定义两个线程ID
// 创建父线程,负责接收来自 A 进程的数据
pthread_create(&tid1, NULL, receive_from_a, NULL);
// 创建子线程,负责发送数据到 A 进程
pthread_create(&tid2, NULL, send_to_a, NULL);
// 等待两个线程完成
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0; // 程序成功结束
}
成果展示:
进程法:
线程法:
三、总结
学习内容概述:
1. 条件变量的同步机制
学习了使用条件变量进行线程同步的基本操作,包括创建、初始化、等待、唤醒和销毁条件变量的函数,以及如何使用条件变量实现生产者-消费者模型。
2. 进程间通信(IPC)
介绍了进程之间的通信方式,尤其是无名管道和有名管道的概念和用法,重点讨论了无名管道的创建与读写特点,有名管道的创建方法。
3. 管道的使用
详细介绍了无名管道的特点和读写机制,包括管道的阻塞与非阻塞行为,以及有名管道的创建和使用。
学习难点:
1. 条件变量的理解和使用
条件变量与互斥锁配合使用,掌握条件变量的核心机制(`pthread_cond_wait` 和 `pthread_cond_signal`)是关键,尤其在生产者-消费者模型中,如何正确处理线程的等待与唤醒,以及避免虚假唤醒。
2. 无名管道的读写特点
管道读写的阻塞条件、数据满与空的处理是难点之一,尤其是在处理写端关闭时的管道破裂情况以及 SIGPIPE 信号的处理。
3. 进程间通信的选择
在多种 IPC 机制中,如何选择最适合的通信方式,如无名管道与有名管道的区别及应用场景,特别是在跨进程的读写同步时的细节处理。
主要事项:
1. 条件变量的同步机制
条件变量通常与互斥锁结合使用,以避免竞态条件。
使用 `pthread_cond_wait` 将线程挂起并释放互斥锁,直到条件变量满足。
使用 `pthread_cond_signal` 唤醒等待线程,确保适时唤醒合适的消费者线程。
2. 无名管道的使用:
无名管道的创建通过 `pipe` 函数,读端和写端的文件描述符需要分别处理读写。
理解管道的读写阻塞机制,尤其在写满管道(64K)时如何阻塞写入,防止数据丢失。
处理管道破裂的情况,当读端不存在时写入会收到 SIGPIPE 信号。
3. 有名管道的使用:
有名管道的创建需要使用 `mkfifo`,并能够持久化存在于文件系统中,便于不同进程之间的通信。
有名管道的特点在于可以跨进程甚至跨系统通信。
未来学习的重点:
1. 条件变量与其他同步机制的结合:
学习如何将条件变量与互斥锁、信号量等其他同步机制结合使用,处理更加复杂的线程同步问题。
2. 高级进程间通信(IPC)机制
如共享内存、消息队列等,通过这些机制提升进程间通信的效率,了解在不同场景中如何选择合适的 IPC 方法。
3. 管道通信的应用场景
深入理解无名管道和有名管道的实际应用场景,特别是在跨进程的生产者-消费者模型中的高效通信。
4. Socket通信:
学习跨主机通信的基础,即基于套接字的进程通信,这是处理分布式系统和网络编程的重要基础。
标签:wbuf,Day31,管道,线程,pthread,进程,rbuf From: https://blog.csdn.net/weixin_65095004/article/details/143093158