详解前驱图与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和按快门两个过程,感觉还是…...
浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)
✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义(Task Definition&…...
AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...
【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...
MODBUS TCP转CANopen 技术赋能高效协同作业
在现代工业自动化领域,MODBUS TCP和CANopen两种通讯协议因其稳定性和高效性被广泛应用于各种设备和系统中。而随着科技的不断进步,这两种通讯协议也正在被逐步融合,形成了一种新型的通讯方式——开疆智能MODBUS TCP转CANopen网关KJ-TCPC-CANP…...
多种风格导航菜单 HTML 实现(附源码)
下面我将为您展示 6 种不同风格的导航菜单实现,每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…...
今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存
文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...
【Linux】Linux 系统默认的目录及作用说明
博主介绍:✌全网粉丝23W,CSDN博客专家、Java领域优质创作者,掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围:SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…...
Vue 3 + WebSocket 实战:公司通知实时推送功能详解
📢 Vue 3 WebSocket 实战:公司通知实时推送功能详解 📌 收藏 点赞 关注,项目中要用到推送功能时就不怕找不到了! 实时通知是企业系统中常见的功能,比如:管理员发布通知后,所有用户…...
大数据治理的常见方式
大数据治理的常见方式 大数据治理是确保数据质量、安全性和可用性的系统性方法,以下是几种常见的治理方式: 1. 数据质量管理 核心方法: 数据校验:建立数据校验规则(格式、范围、一致性等)数据清洗&…...
路由基础-路由表
本篇将会向读者介绍路由的基本概念。 前言 在一个典型的数据通信网络中,往往存在多个不同的IP网段,数据在不同的IP网段之间交互是需要借助三层设备的,这些设备具备路由能力,能够实现数据的跨网段转发。 路由是数据通信网络中最基…...
