从 01 开始 从 01 开始
首页
  • 📚 计算机基础

    • 计算机简史
    • 数字电路
    • 计算机组成原理
    • 操作系统
    • Linux
    • 计算机网络
    • 数据库
    • 编程工具
    • 装机
  • 🎨 前端

    • Node
  • JavaSE
  • Java 高级
  • JavaEE

    • 构建、依赖管理
    • Ant
    • Maven
    • 日志框架
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • 环境管理和配置管理-科普篇
    • Servlet
  • Spring

    • Spring基础
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC

    • SpringMVC 基础
  • SpringBoot

    • SpringBoot 基础
  • Windows 使用技巧
  • 手机相关技巧
  • 最全面的输入法教程
  • 最全面的浏览器教程
  • Office
  • 图片类工具
  • 效率类工具
  • 最全面的 RSS 教程
  • 码字工具
  • 各大平台
  • 校招
  • 五险一金
  • 职场规划
  • 关于离职
  • 杂谈
  • 自媒体
  • 📖 读书

    • 读书工具
    • 走进科学
  • 🌍 英语

    • 从零开始学英语
    • 英语兔的相关视频
    • Larry 想做技术大佬的相关视频
  • 🏛️ 政治

    • 反腐
    • GFW
    • 404 内容
    • 审查与自我审查
    • 互联网
    • 战争
    • 读书笔记
  • 💰 经济

    • 关于税
    • 理财
  • 💪 健身

    • 睡眠
    • 皮肤
    • 口腔健康
    • 学会呼吸
    • 健身日志
  • 🏠 其他

    • 驾驶技能
    • 租房与买房
    • 厨艺
  • 电影

    • 电影推荐
  • 电视剧
  • 漫画

    • 漫画软件
    • 漫画推荐
  • 游戏

    • Steam
    • 三国杀
    • 求生之路
  • 小说
  • 关于本站
  • 关于博主
  • 打赏
  • 网站动态
  • 友人帐
  • 从零开始搭建博客
  • 搭建邮件服务器
  • 本站分享
  • 🌈 生活

    • 2022
    • 2023
    • 2024
    • 2025
  • 📇 文章索引

    • 文章分类
    • 文章归档

晓林

程序猿,自由职业者,博主,英语爱好者,健身达人
首页
  • 📚 计算机基础

    • 计算机简史
    • 数字电路
    • 计算机组成原理
    • 操作系统
    • Linux
    • 计算机网络
    • 数据库
    • 编程工具
    • 装机
  • 🎨 前端

    • Node
  • JavaSE
  • Java 高级
  • JavaEE

    • 构建、依赖管理
    • Ant
    • Maven
    • 日志框架
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • 环境管理和配置管理-科普篇
    • Servlet
  • Spring

    • Spring基础
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC

    • SpringMVC 基础
  • SpringBoot

    • SpringBoot 基础
  • Windows 使用技巧
  • 手机相关技巧
  • 最全面的输入法教程
  • 最全面的浏览器教程
  • Office
  • 图片类工具
  • 效率类工具
  • 最全面的 RSS 教程
  • 码字工具
  • 各大平台
  • 校招
  • 五险一金
  • 职场规划
  • 关于离职
  • 杂谈
  • 自媒体
  • 📖 读书

    • 读书工具
    • 走进科学
  • 🌍 英语

    • 从零开始学英语
    • 英语兔的相关视频
    • Larry 想做技术大佬的相关视频
  • 🏛️ 政治

    • 反腐
    • GFW
    • 404 内容
    • 审查与自我审查
    • 互联网
    • 战争
    • 读书笔记
  • 💰 经济

    • 关于税
    • 理财
  • 💪 健身

    • 睡眠
    • 皮肤
    • 口腔健康
    • 学会呼吸
    • 健身日志
  • 🏠 其他

    • 驾驶技能
    • 租房与买房
    • 厨艺
  • 电影

    • 电影推荐
  • 电视剧
  • 漫画

    • 漫画软件
    • 漫画推荐
  • 游戏

    • Steam
    • 三国杀
    • 求生之路
  • 小说
  • 关于本站
  • 关于博主
  • 打赏
  • 网站动态
  • 友人帐
  • 从零开始搭建博客
  • 搭建邮件服务器
  • 本站分享
  • 🌈 生活

    • 2022
    • 2023
    • 2024
    • 2025
  • 📇 文章索引

    • 文章分类
    • 文章归档
  • 计算机简史

  • 数字电路

  • 计算机组成原理

  • 操作系统

    • 我的操作系统学习笔记

    • 我的操作系统实验笔记

    • 操作系统网课-王道考研

      • 0_课程白嫖指南
      • 1_1_操作系统的概念(定义)、功能和目标
      • 1_2_操作系统的特征
      • 1_3_操作系统的发展与分类
      • 1_4_操作系统的运行机制与体系结构
      • 1_5_中断和异常
      • 2_1_6_系统调用
      • 2_1_1_进程的定义、组成、组织方式、特征
      • 2_1_2_进程的状态与转换
      • 2_1_3_进程控制
      • 2_1_4_进程通信
      • 2_1_5_线程概念和多线程模型
      • 2_2_1_处理机调度的概念、层次
      • 2_2_2_进程调度的时机、切换与过程、方式
      • 2_2_3_调度算法的评价指标
      • 2_2_4_FCFS、SJF、HRRN调度算法
      • 2_2_5_调度算法:时间片轮转、优先级、多级反馈队列
      • 2_3_1_进程同步、进程互斥
      • 2_3_2_进程互斥的软件实现方法
      • 2_3_3_进程互斥的硬件实现方法
      • 2_3_4_信号量机制
      • 2_3_5_用信号量实现进程互斥、同步、前驱关系
      • 2_3_6_生产者-消费者问题
      • 2_3_7_多生产者-多消费者问题
      • 2_3_8_吸烟者问题
      • 2_3_9_读者-写者问题
      • 2_3_10_哲学家进餐问题
      • 2_3_11_管程
        • 管程的定义和基本特征
        • 拓展 1:用管程解决生产者消费者问题
        • 拓展 2:Java 中类似于管程的机制
        • 小结
      • 2_4_1_死锁的概念
      • 2_4_2_死锁的处理策略—预防死锁
      • 2_4_3_死锁的处理策略—避免死锁
      • 2_4_4_死锁的处理策略—检测和解除
      • 3_1_1_内存的基础知识
      • 3_1_2_内存管理的概念
      • 3_1_3_覆盖与交换
      • 3_1_4_连续分配管理方式
      • 3_1_5_动态分区分配算法
      • 3_1_6_基本分页存储管理的基本概念
      • 3_1_7_基本地址变换机构
      • 3_1_8_具有快表的地址变换机构
      • 3_1_9_两级页表
      • 43_3_1_10_基本分段存储管理方式
      • 3_1_11_段页式管理方式
      • 3_2_1_虚拟内存的基本概念
      • 3_2_2_请求分页管理方式
      • 3_2_3_页面置换算法
      • 3_2_4_页面分配策略
      • 4_1_1_初识文件管理
      • 4_1_2_文件的逻辑结构
      • 4_1_3_文件目录
      • 4_1_4_文件的物理结构(上)
      • 4_1_4_文件的物理结构(下)
      • 54_4_1_5_文件存储空间管理
      • 4_1_6_文件的基本操作
      • 4_1_7_文件共享
      • 4_1_8_文件保护
      • 4_1_9_文件系统的层次结构
      • 4_2_1_磁盘的结构
      • 4_2_2_磁盘调度算法
      • 4_2_3_减少磁盘延迟时间的方法
      • 4_2_4_磁盘的管理
      • 5_1.2_I-O 控制器
      • 5_1.3_I-O 控制方式
      • 65_5_1_1_I-O 设备的概念和分类
      • 5_1_4_I-O 软件层次结构
      • 5_1_5_I-O 核心子系统
      • 5_1_6_假脱机技术
      • 5_1_7_设备的分配与回收
      • 5_1_8_缓冲区管理
  • Linux

  • 计算机网络

  • 数据库

  • 编程工具

  • 装机

  • 计算机基础
  • 操作系统
  • 操作系统网课-王道考研
2023-05-15
目录

2311_管程

# 2.3_11_管程

各位同学大家好。在小节中我们会为大家介绍管程相关的内容,首先会从历史发展的角度来让大家了解为什么要引出管程这种机制。

‍

另外管程的定义和基本特征是什么呢,这一点是考试当中比较容易作为选择题考察的一个知识点,最后为了让大家能够更形象的理解管程到底是什么,我们会有两个拓展,第一个拓展是用管程来解决生产者消费者问题。第二个拓展是会介绍一种在 Java 当中类似于管程的机制。

​

那么首先来看一下为什么要引入管程,在管程引入之前,其实人们用来实现进程同步和互斥,主要是使用信号量机制,就是咱们之前学过的那种 pv 操作。但是信号量机制存在的问题就是编写程序困难易出错,这点大家在做题的时候应该也有体会过。比如说咱们在生产者消费者问题当中提到过,如果说实现互斥的 p 操作,在实现同步的 p 操作之前,那么就有可能会引起死锁的状态。比如说如果我们按这样的方式来写生产者和消费者的话,那么按照 123 这样的顺序来执行,这两个进程都会被阻塞,都就会进入一种死锁的状态,所以我们在使用信号量机制的时候,就不得不关心这些 pv 操作的顺序,这就造成了我们在编写程序的时候很困难,并且极易出错的这种问题

所以人们就要想到能不能设计一种机制,能够让程序员写程序的时候,不需要再关注这么复杂的 pv 操作,可以让写代码更加轻松。在 1973 年有一个叫做什么 brain 的人,在 pascal 语言里首次引入了管程的成分,管程其实它是一种高级的同步机制,它本质上也是用于实现进程的互斥同步的,只不过它比之前的这种信号量机制要更方便易用一些,是一种更高级的同步机制。

​ ‍

# 管程的定义和基本特征

那么什么是管程?管程有什么基本特征呢?在聊这个问题之前,咱们必须再次强调,管程其实和之前学过的 pv 操作一样,它也是用来实现进程的互斥和同步的,而进程之间要实现互斥和同步,是因为进程之间可能会共享某些数据资源,比如说像生产者消费者问题当中,生产者和消费者都需要共享的访问缓冲区这种资源

所以为了实现各个进程,对一些共享资源的互斥或者同步的访问的话,那么管程就要有这样一些部分组成。

第一,局部于管程的共享数据结构说明,比如说咱们刚才提到的生产者消费者问题当中,生产者和消费者都需要共享访问的缓冲区,其实我们可以用一种数据结构来表示缓冲区,对缓冲区进行管理,所以在管程当中需要定义一种和这种共享资源相对应的这种共享数据结构。

第二,对上面所提到的这种数据结构进行操作的一组过程,跨考的同学可能不太容易理解什么叫过程,其实过程可以直接理解为它就是所谓的函数。所以第二句话也可以这么说,管程当中还需要定义对之前所提到的这种共享数据结构进行操作的一组函数。

第三,还需要有对局部于管程的共享,数据设置初始值的语句。特别绕,反正就是对这个数据结构要进行初始化的一些语句,也需要在管程当中说明。

第四,管程还需要有一个名字,如果学过面向对象设计的同学可能会发现管程的定义其实就有点类似于我们的类,对吧?在类当中我们可以定义一些数据,并且还可以定义对这些数据进行操作的一组函数一组过程。另外我们还可以在这个类当中定义一些对这些数据进行初始化的语句,当然如果没有学过面向对象语言的同学理解不了也没有关系,咱们之后还会有别的例子来让大家理解。

为了用管程实现进程之间的互斥和同步,管程有这样一些特征

  • 第一,局部与管程的数据只能被局部于管程的过程所访问。

  • 第二,一个进程只有通过调用管程内的过程,才能进入管程访问共享数据,这个有点像英语的长难句,其实他们说的事情也很简单,就是说管程当中定义的这些共享的数据结构,只能被管程当中定义的这一些函数所修改。所以如果我们想要修改管程当中的这一些共享数据结构的话,我们只能通过调用管程提供的这些函数来间接的修改这些数据结构,其实这就是第一句和第二句的意思。

  • 第三,每次仅允许一个进程在管程内执行某个内部过程,就是说管程当中虽然定义了很多函数,但是同一时刻肯定只有一个进程在使用管程当中的某一个函数。别的进程如果也想使用管程当中的某一些函数的话,只要之前的进程还没有用完,别的进程就暂时不能开始执行管程的这些函数,所以这是第三句的意思。

    每次仅允许一个进程在管程内执行某个内部过程,为什么要这么设计?我们可以想一下,比如说我们把生产者消费者问题当中的缓冲区定义为了管程当中的某一种共享数据结构,按照之前咱们学习的内容,我们知道各个进程对缓冲区的访问必须是互斥的,也就是有一个进程在访问缓冲区的时候,别的进程肯定不能同时访问,必须先等待。所以如果我们能够保证每一次仅有一个进程,能在管程当中的某一个内部过程当中执行的话,那么这就意味着每一次对共享数据结构的访问,肯定只有一个进程正在进行,而不可能有多个进程正在同时的访问共享数据结构,所以这就是管程的精髓所在。

​

# 拓展 1:用管程解决生产者消费者问题

接下来我们用一个具体的例子看一下管程是怎么解决生产者消费者问题的,需要注意的是在这个地方并没有按照某一种严格的语法规则来进行表述,这个地方只是为了让大家容易能够理解,所以用了类 C 语言的伪代码来表示管程当中的这一系列逻辑,我们可以用程序设计语言当中提供的某一种特殊的语法,比如说 monitor,end monitor,用这样一对关键字来定义一个管程,就是指中间的这个部分,就是管程的内容

管程的名字叫 ProducerConsumer。另外我们可以定义一些条件变量用来实现同步,还可以定义一些普通的变量,用来表示我们想要记录的信息,比如说缓冲区当中的产品个数。

其实除此之外,我们还需要定义对缓冲区进行描述的一些数据结构,只不过为了方便我们这就省去了。那生产者进程想要往缓冲区里放入一个自己新生产的产品,可以直接调用管程当中定义的这个 insert 函数就可以实现。

像之前咱们用 pv 操作的时候,生产者进程需要有一堆 pv 操作,但如果采用了管程,这个代码就变得特别简洁。首先生产一个产品之后定义管程当中的 insert 函数,然后把自己生产的产品作为这个函数的参数传进去,接下来的问题,生产者进程就不用管了,接下来就由管程来负责解决剩下的什么互斥同步一系列很复杂的问题。

同样的消费者进程也可以很简单的调用管程当中定义的某一个函数,就可以实现从缓冲区当中取出一个产品这样的事情,所以消费者进程的代码也变得非常简洁,而去从缓冲区当中取出一个产品的时候,缓冲区空了怎么办,还要对缓冲区的互斥怎么办,这些消费者进程都不用关心,剩下的都是管程会负责解决的问题。

我们定义了管程之后,在编译的时候,其实会由编译器负责实现各个进程互斥的进入管程当中的过程。这样一件事情举个例子,比如说有两个生产者进程并发的执行,并且先后都调用了管程的 insert 这个过程或者说这个函数。那么由于刚开始没有任何一个进程,正在访问这个管程当中的某一个函数,所以第一个生产者进程在调用 insert 函数的时候是可以顺利的执行下去的,它会执行完一系列代码,包括判断缓冲区是否满了,或者此时是否有一些是消费者进程需要唤醒,这系列的事情都是在管程的 insert 函数里边进行完成的,而如果在第一个进程没有执行完这个函数相应的这一系列逻辑的时候,第二个进程就尝试着也想调用 insert 函数,那么由于编译器实现的这些功能,它会暂时阻止,第二个进程进入 insert 函数。所以就会把第二个进程阻塞在 insert 函数后面,就类似于一个排队器,让他先等待。等第一个进程访问完了 insert 函数之后,才会让第二个进程开始进入 insert 函数,然后执行相应的这一系列逻辑,所以其实互斥的使用某一些共享数据,这是由编译器负责为我们实现的。程序员在写程序的时候不需要再关心如何实现互斥,只需要直接调用管程提供的这一系列的方法,其实它本身就已经能够保证这是互斥的进行的。

​ ‍

那除了互斥之外,管程还可以实现进程的同步。我们可以在管程当中设置一些条件变量,比如说在这个地方我们设置了 full 和 empty 这两个条件变量,还有与他们对应的等待和唤醒操作,用来实现进程的同步问题。

比如说如果有两个消费者进程先执行,生产者进程后执行,那么第一个消费者进程在执行的时候,首先是调用了管程的 remove 过程,或者说这个函数,首先需要判断此时缓冲区里是否有可用的产品,由于刚开始 count 的值本来就是 0,所以第一个消费者进程需要执行 wait,也就是等待操作,于是第一个消费者进程会等待在 empty 条件变量相关的队列当中。

同样的第二个消费者进程开始执行 remove 函数的时候,也会发现此时 count 的值是 0,所以它也需要执行等待操作。同样的也会插入到 empty 条件变量对应的队尾。就像这样子,

​

之后如果有一个生产者进程开始执行,它会执行管程的 insert 函数,或者说这个过程,那么他会把他自己生产的产品放入到缓冲区当中,并且会检查自己放入的产品是不是缓冲区当中的第一个产品,如果说是第一个产品的话,就意味着此时有可能有别的消费者进程正在等待我的产品。所以接下来生产者进程在执行 inside 函数的时候,也会在其中执行一个唤醒操作 signal 功能操作,用于唤醒等待在 Mt 这个条件变量对应的等待队列当中的某一个进程,一般来说都是唤醒排在队头的进程,也就是第一个消费者进程。

由于第一个消费者进程被唤醒了之后,他就可以开始往下执行,首先是执行 count--,让 count 的值由一又变回了 0,然后在检查在自己取走产品之前,缓冲区是不是已经满了?如果说缓冲区之前已经是满的,那么就意味着有可能会有生产者进程需要被唤醒。于是消费者进程又会调用一个对 full 条件变量的 signal,也就是唤醒操作原理,和刚才咱们介绍 empty 这个的原理其实是一样的,最后 remove 函数会返回一个消费者进程想要的产品,对应的一个可以理解为是指针,所以第一个消费者进程就可以通过这样一个步骤就可以取出他想要的产品。

而在取产品的过程当中,如何实现对缓冲区的互斥访问,或者当缓冲区当中没有产品的时候,自己的消费者进程应该怎么处理?这一切都不需要消费者进程再来关心,这一切都是由管程负责解决的。

所以从这个例子当中可以看到,在采用了管程这种机制之后,实现进程的互斥和同步,这些事情就变得简单多了。我们只需要调用管程当中的某一些过程来完成我们想要完成的事情就可以了。

那么我们再根据刚才的例子,用自己的话对管程的特点进行一个描述。

  • 第一,我们需要在管程当中定义一些共享数据,比如说像生产者消费者问题当中的缓冲区对应的数据结构,我们就可以在管程当中定义。
  • 第二,我们可以在管程当中定义一些用于访问这些共享数据的入口,这个入口其实就是所谓的函数或者之前那种说法当中所谓的过程。比如说在生产者消费者问题当中,我们定义了一个 insert 函数和一个 remove 函数,通过这两个入口,我们可以对缓冲区进行操作。
  • 第三,其实我们在生产者消费者进程当中是不可以直接访问共享缓冲区的,我们只能通过管程当中定义的这些特定的入口,也就是它提供的这些函数,才能访问这个共享的缓冲区。
  • 第四管程当中有很多入口,比如说有 insert 入口,有 remove 这样的入口,也就是有很多个函数,但是每一次管程当中只能开放其中的一个入口,并且只能让一个进程或线程进入。所以这种特性就可以保证在一个时间段内最多只会有一个进程在访问我们的共享数据,比如说刚才提到的这种缓冲区,但需要注意的是这种互斥访问管程当中各个入口的这种特性,这种互斥特性它是由编译器负责实现的,程序员其实并不需要关心。
  • 第五,我们可以在管程当中设置一些条件变量和对应的等待唤醒操作来解决同步问题,可以让一个进程或线程在条件变量上等待,比如说咱们刚才举到的在 empty 条件变量上等待的例子,如果一个进程在条件变量上等待的话,进程应该先释放管程的使用权,也就是要让出这个入口。另外我们还可以通过唤醒操作,把等待在条件变量上的进程或者线程唤醒。

那么通过刚才的例子,我们知道程序员其实可以通过某一种特殊的语法来定义一个管程,比如说 monitor 和 end monitor 这样一对关键字之后,其他的程序员就可以通过管程当中定义的这些特殊的入口,或者说这些特定的函数就可以很方便的实现进程的同步和互斥了。

有没有发现其实在管程当中实现了我们之前 pv 操作当中需要实现的什么排队阻塞互斥这一系列的问题,我们只需要简单的调用一个特殊的入口,一个函数就可以很方便的使用了。其实这就是程序设计当中所谓封装的思想,把一些复杂的细节隐藏了,对外只需要提供一个简单易用的接口。

​

# 拓展 2:Java 中类似于管程的机制

那么其实 Java 当中也提供了类似于管程的机制,如果我们用 synchronized 这样一个关键字来定义一个来描述一个函数的话,这个函数就可以保证同一时间段内只能被一个线程所调用。比如说我们定义一个叫做 monitor 的一个类,然后这个类当中的某一个函数,我们用 synchronized 这样一个关键字进行描述,这就意味着 insert 这个函数同一时间段内只能被一个线程所调用,或者说即便多个线程都几乎同时调用了 insert 函数,但后面调用的那些线程是需要排队等待的,只有当前使用 insert 函数的那个线程执行完了相应的代码,最后退出了这个函数之后,下一个线程才可以开始进入这个函数,然后执行其中的代码,这个地方就不再展开细聊了。

对于不熟悉 Java 的同学看不懂也没有关系,这个地方只不过是为了让大家知道管程这个概念其实离我们并不遥远,我们自己也会用到和管程类似的一种机制。只有熟悉了一个概念之后,再遇到与这个概念相关的一些知识点的时候,大家才不会有那种恐惧的感觉。

另外如果熟悉 Java 的同学,在时间允许的情况下,其实也可以自己动手用这个关键字来实尝试实现一下生产者消费者问题当中的管程到底应该怎么定义。

​

# 小结

这个小节我们介绍了管程相关的内容,管程的引入其实就是为了解决信号量机制编程麻烦容易出错的问题。管程其实无非也就是为了实现进程的同步和互斥,只不过是实现起来会更方便一些。

另外我们介绍了管程的组成和基本特征,在考试当中最容易考察的其实是管程的这两个基本特征。首先外部的进程或者线程只能通过管程提供的特定入口,这个入口是什么意思?现在应该已经知道了,其实也就是管程当中定义的某一些函数或者说过程,才可以访问管程当中定义的那些共享数据。另外每一次仅允许一个进程在管程内执行某个内部过程,这两点是最容易在选择题当中进行考察的。

另外我们补充了两个知识点,各进程互斥访问管程的特性其实是由编译器负责实现的,程序员并不需要关心。另外可以在管程当中设置条件变量,还有等待唤醒操作,就可以解决管程的同步问题,管程其实就是应用了封装的思想,把进程同步互斥,这些复杂的细节隐藏在了管程定义的那些函数之内,而对外只提供一个简单易用的函数调用的接口,所以管程是应用了封装的思想

​ ‍

上次更新: 2025/5/9 14:55:39
2_3_10_哲学家进餐问题
2_4_1_死锁的概念

← 2_3_10_哲学家进餐问题 2_4_1_死锁的概念→

最近更新
01
语雀文档一键下载至本地教程
07-04
02
要成功,就不要低估环境对你的影响
07-03
03
血泪教训:电子设备要定期开机
07-02
更多文章>
Theme by Vdoing | Copyright © 2022-2025 | 粤 ICP 备 2022067627 号 -1 | 粤公网安备 44011302003646 号 | 点击查看十年之约
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式