详解前驱图与PV操作
前驱图、PV操作
- 前驱图与PV操作的结合
- 例子:两个进程的同步问题
- 使用PV操作实现同步
- 前驱图的实际应用
- 更复杂的场景
- 示例
- 示例1:前驱图与PV操作的结合
- 1. 前驱图表示
- 2. 使用信号量(PV操作)实现同步
- 进程的执行逻辑:
- 3. 示例代码
- 4. 解释
- 5. 输出结果示例:
- 示例2:信号量分配
- 1. 示例背景
- 2. 信号量的分配
- 3. 信号量初始值:
- 4. 信号量的分配与操作
- 5. 信号量分配的详细步骤
- 6. 初始状态:
- 7. 信号量状态变化的解释:
- 8. 示例代码
- 9. 总结
前驱图(precedence graph)是一种用于表示并发系统中进程或事件之间依赖关系的有向图。在并发编程和同步领域,前驱图用于说明哪些操作必须先发生,哪些可以并行,哪些需要等待其他操作完成。它有助于理解多个进程如何协调执行,避免冲突或死锁等问题。
PV操作是一种经典的进程同步机制,它源自Dijkstra的信号量(Semaphore)理论。PV操作用于管理进程对共享资源的访问,防止并发进程中的竞争条件。P操作(Proberen,尝试)是对信号量减1的操作,如果信号量大于0,进程继续执行;否则,进程阻塞。V操作(Verhogen,增加)是对信号量加1的操作,释放资源,允许其他进程继续执行。
前驱图与PV操作的结合
在并发系统中,前驱图可以用来直观地描述进程的执行顺序和依赖关系,而PV操作用于实现这些依赖关系所需的同步。二者结合使用,能够通过信号量控制进程的执行,使其严格遵循前驱图的依赖顺序。
例子:两个进程的同步问题
假设有两个进程 A
和 B
,它们分别进行以下步骤:
- 进程
A
依次执行操作A1
和A2
- 进程
B
依次执行操作B1
和B2
但是,它们之间有依赖关系:
A2
必须在B1
之后执行,也就是说,A2
的前驱是B1
。
用前驱图来表示这个依赖关系,图中会有一条从 B1
指向 A2
的有向边,表示 A2
依赖于 B1
的完成。
使用PV操作实现同步
我们可以使用信号量 S
来控制 A2
和 B1
之间的执行顺序。初始时,信号量 S=0
,表示 A2
不能立即执行。
- 进程
B
在执行完B1
后,执行V(S)
操作,表示B1
完成并释放信号量。 - 进程
A
在执行A2
前,执行P(S)
操作。由于S
的初始值为0,A2
会阻塞,直到B1
完成并且S
增加到1,A2
才能执行。
前驱图的实际应用
前驱图和PV操作结合的关键在于:
- 前驱图:确定并发系统中事件的依赖顺序。
- PV操作:通过信号量实现这种顺序的同步控制。
通过这种方式,可以确保进程按照预期的顺序执行,避免资源竞争和死锁。例如,在数据库系统中,多个事务对相同数据的并发访问可以通过PV操作控制,保证数据一致性。在操作系统中,多个进程对共享内存的访问也可以通过前驱图建模,并结合PV操作确保正确的同步执行。
更复杂的场景
对于复杂的并发场景,前驱图可能会包含多个有向边,表示多个前驱事件。相应的PV操作可能涉及多个信号量。例如,如果 A3
依赖于 B2
和 C1
的完成,则在 A3
执行前需要等待两个信号量释放。
这种前驱图与PV操作结合的机制不仅限于简单的进程同步,还可以扩展到多进程通信、死锁预防、生产者-消费者问题等各种并发控制问题。
示例
示例1:前驱图与PV操作的结合
假设有三个进程 A
、B
和 C
,并且它们执行的任务如下:
- 进程 A:任务
A1
-> 任务A2
- 进程 B:任务
B1
-> 任务B2
- 进程 C:任务
C1
-> 任务C2
任务之间有如下依赖关系:
A2
必须在B1
完成后才能执行。B2
必须在C1
完成后才能执行。
1. 前驱图表示
我们可以用前驱图表示这些依赖关系:
B1 → A2
:表示A2
依赖B1
,即A2
只有在B1
完成后才能执行。C1 → B2
:表示B2
依赖C1
,即B2
只有在C1
完成后才能执行。
这个前驱图可以画成:
B1 → A2
C1 → B2
2. 使用信号量(PV操作)实现同步
为了实现上述依赖关系,我们引入两个信号量:
- 信号量
S1
,用于控制A2
和B1
的依赖。初始值为 0。 - 信号量
S2
,用于控制B2
和C1
的依赖。初始值为 0。
初始情况下,两个信号量 S1
和 S2
都为 0,表示 A2
和 B2
都不能立即执行,必须等待相应的前驱任务完成。
进程的执行逻辑:
-
进程 A:
- 执行
A1
。 - 执行
P(S1)
操作(等待信号量S1
),当S1 > 0
时,才执行A2
。
- 执行
-
进程 B:
- 执行
B1
。 - 执行
V(S1)
操作,释放信号量S1
,允许A2
执行。 - 执行
P(S2)
操作(等待信号量S2
),当S2 > 0
时,才执行B2
。
- 执行
-
进程 C:
- 执行
C1
。 - 执行
V(S2)
操作,释放信号量S2
,允许B2
执行。
- 执行
3. 示例代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>// 定义信号量
sem_t S1, S2;void* processA(void* arg) {printf("Process A: Executing A1\n");// 等待信号量S1,确保A2在B1之后执行sem_wait(&S1);printf("Process A: Executing A2 after B1\n");return NULL;
}void* processB(void* arg) {printf("Process B: Executing B1\n");// B1完成后,释放信号量S1,允许A2执行sem_post(&S1);// 等待信号量S2,确保B2在C1之后执行sem_wait(&S2);printf("Process B: Executing B2 after C1\n");return NULL;
}void* processC(void* arg) {printf("Process C: Executing C1\n");// C1完成后,释放信号量S2,允许B2执行sem_post(&S2);return NULL;
}int main() {// 初始化信号量sem_init(&S1, 0, 0); // S1 初始为0,A2不能立即执行sem_init(&S2, 0, 0); // S2 初始为0,B2不能立即执行pthread_t threadA, threadB, threadC;// 创建线程,模拟三个进程的执行pthread_create(&threadA, NULL, processA, NULL);pthread_create(&threadB, NULL, processB, NULL);pthread_create(&threadC, NULL, processC, NULL);// 等待线程完成pthread_join(threadA, NULL);pthread_join(threadB, NULL);pthread_join(threadC, NULL);// 销毁信号量sem_destroy(&S1);sem_destroy(&S2);return 0;
}
4. 解释
-
信号量的初始化:
sem_init(&S1, 0, 0)
和sem_init(&S2, 0, 0)
将信号量S1
和S2
初始化为 0。表示A2
和B2
不能立即执行,必须等待它们的依赖任务完成。 -
进程 A:
A1
立即执行,执行到A2
时,它会阻塞在sem_wait(&S1)
处,直到B1
完成并释放S1
。 -
进程 B:
B1
执行后调用sem_post(&S1)
,释放信号量S1
,允许A2
执行。B2
需要等待C1
完成,调用sem_wait(&S2)
进行同步。 -
进程 C:
C1
执行后调用sem_post(&S2)
,释放信号量S2
,允许B2
执行。
5. 输出结果示例:
Process B: Executing B1
Process A: Executing A1
Process C: Executing C1
Process A: Executing A2 after B1
Process B: Executing B2 after C1
示例2:信号量分配
1. 示例背景
假设我们有三个进程 A
、B
和 C
,它们执行的任务如下:
- 进程 A:
A1 -> A2
- 进程 B:
B1 -> B2
- 进程 C:
C1 -> C2
任务之间有以下依赖关系:
A2
必须在B1
完成后执行。B2
必须在C1
完成后执行。
2. 信号量的分配
我们将使用信号量 S1
和 S2
来同步这些依赖关系:
- 信号量
S1
用于控制A2
和B1
之间的依赖。 - 信号量
S2
用于控制B2
和C1
之间的依赖。
3. 信号量初始值:
S1 = 0
:表示A2
在B1
完成之前不能执行。S2 = 0
:表示B2
在C1
完成之前不能执行。
4. 信号量的分配与操作
-
初始状态:所有信号量的初始值为
0
,表示依赖任务尚未完成,相关任务需要等待前驱任务完成后才能执行。 -
进程 A:
A1
任务可以立即执行,不依赖任何其他任务。A2
任务需要等待B1
任务完成。我们使用sem_wait(S1)
来阻塞A2
的执行,直到B1
释放信号量S1
。
-
进程 B:
B1
任务可以立即执行,不依赖其他任务。- 执行完
B1
后,进程 B 调用sem_post(S1)
,将S1
的值从0
增加到1
,释放信号量,使A2
能够继续执行。 B2
任务依赖于C1
任务的完成,因此在B2
任务执行前调用sem_wait(S2)
,阻塞B2
,直到C1
完成并释放信号量S2
。
-
进程 C:
C1
任务可以立即执行,不依赖任何其他任务。- 执行完
C1
后,进程 C 调用sem_post(S2)
,将S2
的值从0
增加到1
,允许B2
执行。
5. 信号量分配的详细步骤
为了使这个过程更加清晰,让我们以具体的执行步骤和信号量状态变化为例:
6. 初始状态:
S1 = 0
(A2
需要等待B1
完成)S2 = 0
(B2
需要等待C1
完成)
操作 | 说明 | S1 信号量状态 | S2 信号量状态 |
---|---|---|---|
A1 执行 | 无需等待,直接执行 | 0 | 0 |
B1 执行 | 无需等待,直接执行 | 0 | 0 |
sem_post(S1) | B1 完成,释放 S1 ,允许 A2 执行 | 1 | 0 |
A2 执行 | A2 依赖 B1 ,S1 = 1 ,可以执行 | 0 | 0 |
C1 执行 | 无需等待,直接执行 | 0 | 0 |
sem_post(S2) | C1 完成,释放 S2 ,允许 B2 执行 | 0 | 1 |
B2 执行 | B2 依赖 C1 ,S2 = 1 ,可以执行 | 0 | 0 |
7. 信号量状态变化的解释:
- 初始状态:
S1 = 0
和S2 = 0
,这意味着A2
和B2
不能立即执行。 B1
完成后:B1
执行完毕后,进程 B 通过sem_post(S1)
将信号量S1
增加到 1,表示A2
可以执行。A2
执行:A2
检查S1
是否大于 0,发现信号量已被释放,于是继续执行。C1
完成后:C1
执行完毕后,进程 C 通过sem_post(S2)
将信号量S2
增加到 1,表示B2
可以执行。B2
执行:B2
检查S2
是否大于 0,发现信号量已被释放,于是继续执行。
8. 示例代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>// 定义信号量
sem_t S1, S2;void* processA(void* arg) {printf("Process A: Executing A1\n");// 等待信号量S1,确保A2在B1之后执行sem_wait(&S1);printf("Process A: Executing A2 after B1\n");return NULL;
}void* processB(void* arg) {printf("Process B: Executing B1\n");// B1完成后,释放信号量S1,允许A2执行sem_post(&S1);// 等待信号量S2,确保B2在C1之后执行sem_wait(&S2);printf("Process B: Executing B2 after C1\n");return NULL;
}void* processC(void* arg) {printf("Process C: Executing C1\n");// C1完成后,释放信号量S2,允许B2执行sem_post(&S2);return NULL;
}int main() {// 初始化信号量sem_init(&S1, 0, 0); // S1 初始为0,A2不能立即执行sem_init(&S2, 0, 0); // S2 初始为0,B2不能立即执行pthread_t threadA, threadB, threadC;// 创建线程,模拟三个进程的执行pthread_create(&threadA, NULL, processA, NULL);pthread_create(&threadB, NULL, processB, NULL);pthread_create(&threadC, NULL, processC, NULL);// 等待线程完成pthread_join(threadA, NULL);pthread_join(threadB, NULL);pthread_join(threadC, NULL);// 销毁信号量sem_destroy(&S1);sem_destroy(&S2);return 0;
}
9. 总结
- **信
号量分配的关键在于将每个信号量与特定的任务依赖关系相关联,以确保进程按照正确的顺序执行。
在这个示例中:
S1
控制A2
任务在B1
任务完成后执行。S2
控制B2
任务在C1
任务完成后执行。
通过初始化信号量为 0
,我们确保依赖任务(如 A2
和 B2
)不会提前执行,只有在前驱任务完成并释放信号量时,依赖任务才能继续运行。
这种信号量的分配和使用可以广泛应用于多进程或多线程程序中,帮助有效地管理复杂的同步问题,避免竞争条件和不确定的执行顺序。
相关文章:

详解前驱图与PV操作
前驱图、PV操作 前驱图与PV操作的结合例子:两个进程的同步问题使用PV操作实现同步 前驱图的实际应用更复杂的场景示例示例1:前驱图与PV操作的结合1. 前驱图表示2. 使用信号量(PV操作)实现同步进程的执行逻辑: 3. 示例代…...

孩子来加拿大上学真的那么轻松吗?(上)
点击文末“阅读原文”即可参与节目互动 剪辑、音频 / 卷圈 运营 / SandLiu 卷圈 监制 / 姝琦 封面 / 姝琦Midjourney 产品统筹 / bobo 这是拼娃时代第三十一期节目,经过了一年的沉寂,拼娃时代在今年九月份终于恢复更新啦,JunJun老师也…...

【算法篇】二叉树类(1)(笔记)
目录 一、认识二叉树 1. 二叉树的种类 (1)满二叉树 (2)完全二叉树 (3)二叉搜索树 (4)平衡二叉搜索树 2. 二叉树的存储方式 3. 二叉树的遍历方式 4. 二叉树的定义 二、Leet…...

《C++无锁编程:解锁高性能并发的新境界》
在当今的软件开发领域,并发编程的重要性日益凸显。随着多核处理器的普及,开发者们越来越需要利用并发来提高程序的性能和响应速度。而 C作为一种强大的编程语言,提供了多种技术来实现无锁编程,从而在并发环境下获得更高的性能和更…...

系统架构设计师教程 第9章 9.5 软件可靠性测试 笔记
9.5 软件可靠性测试 ★★★☆☆ 9.5.1 软件可靠性测试概述 软件测试者可以使用很多方法进行软件测试,如按行为或结构来划分输入域的划分测试, 纯粹随机选择输入的随机测试,基于功能、路径、数据流或控制流的覆盖测试等。 软件可靠性测试由可…...

如何使用ssm实现校园体育赛事管理系统的设计与实现+vue
TOC ssm713校园体育赛事管理系统的设计与实现vue 绪论 课题背景 身处网络时代,随着网络系统体系发展的不断成熟和完善,人们的生活也随之发生了很大的变化。目前,人们在追求较高物质生活的同时,也在想着如何使自身的精神内涵得…...

CSS 中的文本相关属性(line - height、font、letter - 属性、text - 属性)
目录 非 VIP 用户可前往公众号回复“css”进行免费阅读 line - height属性 字号与行高的取值约定 行高与盒子高度的关系 font、letter -属性 、text -属性 font属性 letter -属性 text - 属性 非 VIP 用户可前往公众号回复“css”进行免费阅读 line - height属性 字号与…...

mobaxterm、vscode通过跳板机连接服务器
目标服务器:111.111.11.11 跳板机:100.100.10.10 1. mobaxterm通过跳板机连接服务器 1.1 目标服务器信息 1.2 跳板机信息 1.3 登录 点击登录,会输入密码,成功 参考:https://blog.csdn.net/qq_40636486/article/det…...

鸿萌数据恢复:iPhone 手机被盗后应采取哪些措施?警惕这些骗局
天津鸿萌科贸发展有限公司从事数据安全服务二十余年,致力于为各领域客户提供专业的数据恢复、数据备份解决方案与服务,并针对企业面临的数据安全风险,提供专业的相关数据安全培训。 丢失昂贵的 iPhone 不仅会造成较大的经济损失,还…...

为了学习Python熬夜部署了Jupyter Notebook 6.x
文章目录 Docker拉取并构建容器安装部署jupyter对Jupyter使用过程问题总结1 没有代码提示怎么办?2 如果想切换python版本了怎么办?3 想在jupyter里面使用vim怎么办? 遇见的问题参考文章 怎么说,今天在学习Python的时候,…...

docker-文件复制(docker cp:用于在Docker主机和容器之间拷贝文件或目录)
文章目录 1、把宿主机的文件复制到容器内部1.1、查询 宿主机 root 下的文件1.2、docker cp /root/anaconda-ks.cfg spzx-redis:/root1.3、查看 spzx-redis 容器 中/root目录下是否有 anaconda-ks.cfg 文件 2、把容器中的文件 复制 到宿主机中2.1、查看 spzx-redis 容器 / 下的文…...

guava里常用功能
guava 是 Google 提供的一个 Java 库,提供了很多实用的工具类和方法,可以帮助开发者更高效地编写代码。以下是一些常用的 Guava 工具类及其功能示例: 1. Lists 用于操作列表的工具类。 import com.google.common.collect.Lists;List<In…...

su 命令:一键切换用户身份、提高su命令安全性的建议
一、命令简介 su 命令是 Linux 和 Unix 系统中的一个实用工具,用于切换用户身份。它允许当前登录用户在不退出登录会话的情况下,切换到另一个用户的身份。通常,su 用于从普通用户切换到 root 用户,或从 root 用户切换到其他…...

观察者模式(发布-订阅模式)
用途: (1)可用于拦截过滤器 (2)订单创建成功后的一些后续逻辑(消息提醒,订单打印,物品打包等) (3)需要由统一调度中心调度的一系列任务等 消息…...

耦合微带线单元的网络参量和等效电路公式推导
文档下载链接:耦合微带线单元的网络参量和等效电路资源-CSDN文库https://download.csdn.net/download/lu2289504634/89583027笔者水平有限,错误之处欢迎留言! 一、耦合微带线奇偶模详细推导过程 二、2,4端口开路 三、2端口短路、3端口开路 四…...

elasticsearch的Ingest Attachment插件的使用总结
安装 Ingest Attachment 插件 确保 Elasticsearch 已安装: 首先,请确保你已经安装并运行了 Elasticsearch。可以通过访问 http://localhost:9200 来检查是否正常运行。 安装插件: 使用以下命令在 Elasticsearch 中安装 Ingest Attachment 插…...

SemiDrive E3 MCAL 开发系列(4) – Gpt 模块的使用
一、 概述 本文将会介绍SemiDrive E3 MCAL GPT模块的基本配置,并且会结合实际操作的介绍,帮助新手快速了解并掌握这个模块的使用,文中的 MCAL 是基于 PTG3.0 的版本,开发板是官方的 E3640 网关板。 二、 Gpt 模块的主要配置 …...

前端导出页面PDF
import html2canvas from html2canvas import { jsPDF } from jspdf import { Loading } from element-ui let downloadLoadingInstance// 导出页面为PDF格式---使用插件html2canvas和jspdf插件 export function exportPDF(fileName, node) {downloadLoadingInstance Loading.…...

Jenkins的安装
1.简介 官网:https://www.jenkins.io 中文文档:Jenkins Jenkins 是一个开源的持续集成(CI)工具,用于自动化构建、测试和部署软件项目。它提供了一个易于使用和可扩展的平台,帮助团队更高效地开发和交付软…...

初学51单片机之I2C总线与E2PROM
首先先推荐B站的I2C相关的视频I2C入门第一节-I2C的基本工作原理_哔哩哔哩_bilibili 看完视频估计就大概知道怎么操作I2C了,他的LCD1602讲的也很不错,把数据建立tsp和数据保持thd,比喻成拍照时候的摆pose和按快门两个过程,感觉还是…...

C语言数组探秘:数据操控的艺术【下】
承接上篇,我们继续讲数组的内容。 八.二维数组的使用 当我们掌握了二维数组的创建和初始化,那我们怎么使用二维数组呢?其实二维数组访问也是使用下标的形式的,二维数组是有行和列的,只要锁定了行和列就能唯一锁定数组中…...

Jmeter关联,断言,参数化
目录 一、关联 边界提取器 JSON提取器 正则表达式提取器 跨线程关联 二、断言 响应断言 JSON断言 断言持续时间 三、参数化 用户参数 csv data setconfig csvread函数 一、关联 常用的关联有三种 1.边界提取器 2.JSON提取器 3.正则表达式提取器 接下来就详细讲述…...

嵌入式单片机底层原理详解
前言 此笔记面向有C语言基础、学习过数字电路、对单片机有一定了解且尚在学习阶段的群体编写,笔记中会介绍单片机的结构、工作原理,以及一些C语言编程技巧,对于还停留在复制模板、copy代码阶段的读者会有比较大的帮助,待学习完成后可以独立完成几乎所有单片机的驱动开发。 …...

重修设计模式-行为型-责任链模式
重修设计模式-行为型-责任链模式 将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。 责任链模式(Chain of Responsibilit…...

Vercel部署/前端部署
Vercel 部署 今天要讲的是如何对别人向自己的开源仓库提的PR进行自动代码审核 1. 注册并登录Vercel 访问 Vercel官网点击右上角的"Sign Up"选择使用GitHub、GitLab、Bitbucket或邮箱注册完成注册流程并登录 2. 连接代码仓库 在Vercel仪表板,点击"New Proje…...

常见的css预处理器
CSS预处理器是一种扩展了CSS功能的脚本语言,它允许开发者以编程的方式编写更加干净、结构化的CSS代码。通过引入变量、嵌套规则、混合(Mixins)、函数等高级特性,CSS预处理器使得CSS代码的编写更加灵活、高效,同时也提高…...

mysql—半同步模式
mysql的并行复制 在172.25.254.20(slave)主机上 默认情况下slave中使用的是sql单线程回放 在master中时多用户读写,如果使用sql单线程回放那么会造成组从延迟严重 开启MySQL的多线程回放可以解决上述问题 mysql> show processlist; 在配置文件中进行编辑 [root…...

You are not allowed to push code to this project
原因1 用户权限不够。 具体查看用户权限路径: 原因2 vscode之前都能提交代码,但是突然就提交不上了。 表现为:前端代码能拉取,但是不能提交。使用idea进行前端代码的提交,完全没问题。 解决方案:修改TortoiseG…...

Java刷题:最小k个数
目录 题目描述: 思路: 具体实现 整体建立一个大小为N的小根堆 通过大根堆实现 完整代码 力扣链接:面试题 17.14. 最小K个数 - 力扣(LeetCode) 题目描述: 设计一个算法,找出数组中最小的…...

Redis实战--Redis应用过程中出现的热门问题及其解决方案
Redis作为一种高性能的key-value数据库,广泛应用于缓存、消息队列、排行榜等场景。然而,在实际应用中,随着业务规模的不断扩大和访问量的持续增长,缓存系统也面临着诸多挑战,其中最为典型的便是缓存穿透、缓存击穿和缓…...