前言

如果你对这篇文章可感兴趣,可以点击「【访客必读 - 指引页】一文囊括主页内所有高质量博客」,查看完整博客分类与对应链接。

文章目录

  • 前言
  • 六、并发程序设计
    • 6.1 并发进程
      • 6.1.1 并发程序设计
      • 6.1.2 并发程序设计
    • 6.2 临界区
      • 6.2.1 临界区概念
      • 6.2.2 临界区两个错误尝试
      • 6.2.3 TS 指令(不可行)
      • 6.2.4 临界区开关中断(不可行)
      • 6.2.5 PV 操作(信号量)
    • 6.3 进程同步
      • 6.3.1 生产者消费者问题概述
      • 6.3.2 1生产,1消费,1缓冲
      • 6.3.3 1生产,1消费,N缓冲
      • 6.3.4 N生产,N消费,N缓冲
      • 6.3.5 苹果橘子问题
    • 6.4 管程
      • 6.4.1 管程概念
      • 6.4.2 霍尔管程
      • 6.4.3 哲学家问题
      • 6.4.4 读者写者问题
    • 6.5 进程通信
      • 6.5.1 进程通信概念
      • 6.5.2 进程直接通信
      • 6.5.3 进程间接通信
      • 6.5.4 基于字节流的通信规约
      • 6.5.5 基于RPC的高级通信规约
    • 6.6 死锁
      • 6.5.1 死锁的定义
      • 6.5.2 死锁产生的四个必要条件
      • 6.5.3 死锁的防止
      • 6.5.4 死锁的避免
      • 6.5.5 死锁的检测

六、并发程序设计

6.1 并发进程

6.1.1 并发程序设计

并发程序设计: 把一个具体问题求解设计成若干个可同时执行的程序模块的方法

并发设计举例

先是最简单的顺序程序设计。

不难发现,处理器利用率为,5278+52+20≈35%\displaystyle\frac{52}{78+52+20}\approx 35\%78+52+2052​≈35%。因此我们对上述过程进行改进,引入并发程序设计,如下所示。

在这种设计方式中,我们可以得到处理器利用率为,52∗n78∗n+52+20≈67%\displaystyle\frac{52*n}{78*n+52+20}\approx 67\%78∗n+52+2052∗n​≈67%。

特性

从上述例子中,我们很明显地看到了并发程序设计的优势,因此我们来总结这种程序设计方式的特性。

  • 并行性: 多个进程在多道程序系统中并发执行或在多处理器系统中并行执行

    • 提高了计算效率
  • 共享性: 多个进程共享软件资源
  • 交往性: 多个进程并发执行时存在制约,增加了程序设计的难度

就是这个交往性导致了并发程序设计中关于进程互斥与同步的问题。

6.1.2 并发程序设计

无关与交往的并发进程

无关的并发进程:

  • 一组并发进程分别在不同的变量集合上运行,一个进程的执行与其他并发进程的进展无关。

交往的并发进程:

  • 一组并发进程共享某些变量,一个进程的执行可能影响其他并发进程的结果。

进程互斥问题

进程互斥: 并发进程之间因相互争夺独占性资源而产生的竞争制约关系

如在上图的 if 部分,进程 1 执行到此处恰好被中断,Xi 还没有变化,因此进程 2 进入卖掉了票。之后轮到进程 1 执行,进程 1 也把票卖了,造成了错误。

进程同步问题

进程同步: 并发进程之间为完成共同任务基于某个条件来协调执行先后关系而产生的协作制约关系。

  • 同步的关键在于等待。
  • 例如计算进程必须在输入进程输入结束之后,才能对输入结果进行处理。

如上述的进程获取与释放资源的同步过程。在分配函数中,进程 1 进入 if,但没有进入队列时被中断。此时进程 2 释放了所有进程,然后回到进程 1 进入等待队列,此时进程 1 将会永远等待。

6.2 临界区

6.2.1 临界区概念

临界资源

临界资源: 互斥共享变量所代表的资源,即一次只能被一个进程使用的资源。

临界区

临界区: 并发过程中与互斥共享变量相关的程序段。

如果两个进程同时停留在相关的临界区内,就会出现进程互斥或同步问题。

  • 两个进程的临界区有相同的临界资源,就是相关的临界区,必须互斥进入。
  • 两个临界区不相关,进入则没有限制。

临界区的描述

  • 确定临界资源
shared <variable>
  • 确定临界区
region <variable> do <statement_list>

临界区管理的三个要求

  1. 一次至多允许一个进程停留在相关的临界区
  2. 一个进程不能无限止地停留在临界区内
  3. 一个进程不能无限止地等待进入临界区(避免饿死)

临界区的嵌套使用

如果要对临界区进行嵌套使用,则必须规定各临界区资源的优先级,否则就会出现下述的死锁情况。

6.2.2 临界区两个错误尝试

错误想法: 利用一个标志量来标记进程是否在临界区中,并与 while 一同使用。

尝试一

看上去没有问题,但是会导致两个进程同时进入临界区。

  1. P1 刚执行完 while,发生中断,P2 执行
  2. P2 刚进入临界区,发生中断,P1 执行
  3. P1 进入临界区,此时两个进程同时进入临界区

尝试二

下述方式依然存在问题,两个进程会陷入死锁。

  1. P1 刚执行完 inside1 的赋值,被中断,P2 执行
  2. P2 执行完 inside2 的赋值后,P1 与 P2 陷入死锁

根据两次尝试,我们可以发现上述的赋值和 while 两条指令的执行过程不能发生中断,否则就会出现错误。

6.2.3 TS 指令(不可行)

因此我们将赋值与 while 两条指令进行合并,得到了测试并建立指令。

或稍微进行改进,形成 swap 指令。

不难发现,两条指令虽然保证程序不会出问题,但会导致忙式等待,非常降低程序效率。

6.2.4 临界区开关中断(不可行)

还有一个非常简单的方法是在进出临界区时开关中断,这样临界区执行就不会发生中断了,执行具有原子性。

关中断;临界区
开中断;

缺点

这种方式仅适用于系统内部程序的临界区操作手段,而且要保证指令长度短小精悍。

如果把开关中断权限开放给用户,可能会发生滥用,即造成巨大破坏性。

6.2.5 PV 操作(信号量)

为了改进 TS 或开关中断方式,我们引入 PV 操作。

信号量

核心数据结构: 等待进程队列

信号量声明: 资源报到,建立队列

申请资源原语: 若申请不到,调用进程入队等待

归还资源原语: 若队列中有等待进程,需释放

信号量撤销: 资源注销,撤销队列

typedef struct semaphore{int value;          // 信号量值struct pcb *list;   // 等待进程队列指针
}
  • 每个信号量建立一个等待进程队列
  • 每个信号量对应一个整数值
    • 正值表示资源可复用次数
    • 0 值表示无资源且无进程等待
    • 负值表示资源等待队列中进程个数

PV 操作

procedure P(semaphore:s) {s = s - 1;          // 信号量减 1if (s < 0) W(s);    // 若信号量小于 0,则进程进入等待队列
}
procedure V(semaphore:s) {s = s + 1;          // 信号量加 1if (s <= 0) R(s);   // 若信号量小于等于 0,则释放等待队列中一个进程
}

我们可以利用 PV 操作来解决进程互斥问题。

Semaphore s;
s = 1;
cobegin
process Pi {...P(s);临界区V(s);...
}
coend;

PV 操作解决机票问题

int A[m];
Semaphore s;
s = 1;
cobegin
process Pi {Li: 按旅客订票要去找到 A[j]P(s);if ( A[j] >= 1 ) { A[j]--;V(s);// 输出一张票}else {V(s);// 输出票已售完}goto Li;
}
coend;

注意上述代码中,P 操作与 V 操作在执行路径上一一匹配。除此之外,上述代码还有一个问题,即只有相同航班的票数才是相关的临界资源,因此用一个信号量处理全部机票会影响进程并发度,下述代码为修改后的代码。

int A[m];
Semaphore s[m];
for (int j = 0; j < m; j++) s[j] = 1;
cobegin
process Pi {L1: 按旅客订票要去找到 A[j]P(s[j]);if ( A[j] >= 1 ) { A[j]--;V(s);// 输出一张票}else {V(s);// 输出票已售完}goto Li;
}
coend;

6.3 进程同步

进程同步: 并发进程为完成共同任务基于某个条件来协调执行先后关系而产生的协作制约关系

  • 一个进程的执行等待来自于其他进程的消息

6.3.1 生产者消费者问题概述

生产者、消费者共享缓冲区。

  • 缓冲区有空位时,生产者可放入产品,否则等待
  • 缓冲区有产品时,消费者可取出产品,否则等待

6.3.2 1生产,1消费,1缓冲

6.3.3 1生产,1消费,N缓冲

缓冲区扩大了,因此需要加上缓冲区首尾指针。

6.3.4 N生产,N消费,N缓冲

由于生产者和消费者数量增加,因此缓冲区的首尾指针也需要保证互斥性。

该问题有个注意点,先同步后互斥,否则会死锁。如果是先互斥再同步的话,会出现生产者把缓冲区生产满了,此时先拿锁,然后进入等待队列,而消费者拿不到锁也没办法消费,导致死锁。

6.3.5 苹果橘子问题

如果盘子里面可以放多个水果的话,首先扩大缓冲区,其次令 sp = n,然后设置一个缓冲区锁,每次只能有一个人放或取东西。每次拿或取的时候采用遍历搜寻的方式。

接下来需要考虑 P(s) 放的位置,P(s) 互斥操作要在同步操作之后,否则会发生错误。V(s) 操作无先后关系。

6.4 管程

6.4.1 管程概念

概念提出

管程本质是对信号量的类封装,简化操作。

  • 管程试图抽象相关并发进程对共享变量访问,以提供一个友善的并发程序设计开发环境。
  • 管程是由若干公共变量及其说明和所有访问这些变量的过程所组成。
  • 管程把分散在各个进程中互斥地访问公共变量的那些临界区集中起来管理,管程的局部变量只能由该管程的过程存取。
  • 进程只能互斥地调用管程中的过程。

管程的条件变量

  • 条件变量: 当调用管程过程的进程无法运行时,用于阻塞进程的信号量
  • 同步原语 wait: 当一个管程过程发现无法继续时,它在某些条件变量上执行 wait,引起进程阻塞
  • 同步原语 signal: 用于释放在条件变量上阻塞的进程

执行模型

注意,管程中一共有 3 个等待进程队列。由于进程只能互斥地调用管程中的过程,因此有了互斥调用管程过程队列。

又因为当前在管程中的进程一旦调用 signal,则会有两个进程在管程中,所以又有一个更高优先级的互斥调用队列。

signal 问题

当使用 signal 释放一个等待进程时,可能出现两个进程同时停留在管程内,此时有三种解决方法。

  1. 执行 signal 的进程等待,直到被释放进程退出管程或等待另一个条件(霍尔管程)
  2. 被释放进程等待,直到执行 signal 的进程退出管程或等待另一个条件
  3. 规定管程中的 signal 操作时过程体的最后一个操作(汉森法,增加程序设计难度)

6.4.2 霍尔管程

霍尔管程

使用 signal 释放一个等待进程时,霍尔管程让执行 signal 的进程等待,直到被释放进程退出管程或等待另一个条件。

霍尔管程基于 PV 操作原语实现。

  • Wait 和 Signal 可以是程序过程
  • 不需要扩展 OS 内核

信号量

两个互斥信号量。

if (IM.next_count > 0) V(IM.next);
else V(IM.mutex);

条件变量

x_sem: semaphore;   // 与资源相关的信号量
x_count: integer;   // 在 x_sem 上等待的进程数

Wait

void Wait(semaphore, x_count, IM){x_count++;  // 等待数加上自己if(IM.next_count > 0) V(IM.next);else V(IM.mutex);P(x_sem);   // 挂起自己x_count--;  // 等待数减去自己
}

Signal

void Signal(semaphore, x_count, IM){if(x_count > 0){IM.next_count++;    // 进程队列加入自己V(x_sem);           // 信号量操作P(IM.next);         // 将自己阻塞住,先执行被 signal 的进程IM.next_count--;    // 进程队列去除自己}
}

6.4.3 哲学家问题

问题描述

霍尔管程方法

其中 s 是盘子状态。

6.4.4 读者写者问题

问题描述

霍尔管程

6.5 进程通信

6.5.1 进程通信概念

  • 交往进程通过信号量操作实现进程互斥和同步,这是一种低级通信方式。

  • 进程有时还需要交换更多的信息(如何把数据传送给另一个进程),可以引入高级通信方式 —— 进程通信机制,实现进程间用信件来交换信息。

  • 进程通信扩充了并发进程的数据共享。

6.5.2 进程直接通信

发送或接收信件的进程指出信件发给谁,或从谁那里接收信件。

  • send(P, 信件): 把信件发送给进程 P
  • receive(Q, 信件): 从进程 Q 接收信件

6.5.3 进程间接通信

间接通信概念

  • 发送或者接收信件通过一个信箱来进行,该信箱有唯一标识符

  • 多个进程共享一个信箱

    • send(A, 信件): 把信件传送到信箱 A
    • receive(A, 信件): 从信箱 A 接收信件

信箱

信箱是存放信件的存储区域,每个信箱可以分成信箱特征和信箱体两部分。

  • 信箱特征: 信箱容量、信件格式、指针
  • 信箱体: 分成若干个区,每个区可容纳一封信

发送信件原语流程

  1. 若指定的信箱未满,则把信件送入信箱中指针所指示的位置,释放等待该信箱中信件的等待者
  2. 否则,发送信件者被置成等待信箱的状态

接收信件原语流程

  1. 若指定信箱中有信件,则取出一封信,释放等待信箱的等待者
  2. 否则,接收信件者被置成等待信箱中信件的状态

6.5.4 基于字节流的通信规约

  • 多个进程使用一个共享的消息缓冲区(管道、多路转接器、套接字)
  • 一些进程往消息缓冲区中写入字符流
  • 一些进程从消息缓冲区中读出字符流
  • 信息交换单位基于字符流,长度任意

6.5.5 基于RPC的高级通信规约

  • 采用客户/服务器计算模式
  • 服务器进程提供一系列过程/服务,供客户进程调用
  • 客户进程通过调用服务器进程提供的过程/服务获得服务
  • 考虑到客户计算机和服务器计算机的硬件异构型,外部数据表示 XDR 被引入来转换每台计算机的特殊数据格式为标准数据格式

6.6 死锁

这里需要主要一下死锁防止和死锁避免的区别,下面给出一段英文描述。

The main difference between deadlock prevention and deadlock avoidance is that deadlock prevention ensures that at least one of the necessary conditions to cause a deadlock will never occur while deadlock avoidance ensures that the system will not enter an unsafe state.

关键点在于死锁防止是从根本上解决死锁问题,而死锁避免是人为地尽可能避开,还是有出意外的可能。

6.5.1 死锁的定义

一组进程处于死锁状态是指,每一个进程都在等待被另一个进程所占有的、不能抢占的资源。

  • 存在 nnn 个进程 P1,P2,...,PnP_1,P_2,...,P_nP1​,P2​,...,Pn​
  • 进程 PiP_iPi​ 因为申请不到资源 RiR_iRi​ 而处于等待状态
  • 而 RiR_iRi​ 又被 Pi+1P_{i+1}Pi+1​ 占有,RnR_nRn​ 被 P1P_1P1​ 占有
  • 显然,这 nnn 个进程的等待状态永远不能结束,这 nnn 个进程就处于死锁状态

6.5.2 死锁产生的四个必要条件

  1. 互斥条件: 进程应互斥使用资源,任一时刻一个资源仅为一个进程独占
  2. 占有和等待条件: 一个进程请求资源得不到满足而等待时,不释放已占有的资源
  3. 不剥夺条件: 任一进程不能从另一进程那里抢夺资源
  4. 循环等待条件: 存在一个循环等待链,每一个进程分别等待它前一个进程所持有的资源

6.5.3 死锁的防止

独占型资源改为共享(破坏条件1)

独占变共享,可以破坏条件 1,但这对许多资源往往是不能做到的。

剥夺式调度(破坏条件3)

剥夺式调度目前只适用于对主存资源和处理器资源的分配,而不适用于所有资源。

静态分配(预分配 - 破坏条件2)

具体方法: 一个进程必须在执行前就申请它所要的全部资源,并且直到它所要的资源都得到满足之后才开始执行。

缺点: 可能会出现一种情况。进程执行时,资源 A 只使用了一小段时间,但在整个执行过程中,资源 A 始终被占用,造成资源利用率的低效。

层次分配(破坏条件4)

具体方法: 将资源分成多个层次。一个进程得到某一层的一个资源后,它只能再申请在较高层的资源,当一个进程要释放某层的一个资源时,必须先释放所占用的较高层的资源。当一个进程获得了某一层的一个资源后,它想再申请该层中的另一个资源,那么必须先释放该层中的已占资源。

缺点: 低层次资源使用效率依然较低。

6.5.4 死锁的避免

当不能防止死锁的产生时,如果能掌握并发进程中与每个进程有关的资源申请情况,仍然可以避免死锁的发生。

只需在为申请者分配资源前先测试系统状态,若把资源分配给申请者会产生死锁的话,则拒绝分配,否则接收申请,为它分配资源。

银行家算法: 借钱给有偿还能力的客户

  • 系统首先检查申请者对资源的最大需求量,如果现存的资源可以满足它的最大需求量时,就满足当前的申请
  • 即仅仅在申请者可能无条件地归还它所申请的全部资源时,才分配资源给它

注意: 这种判断不仅是对单个用户的判断,而是对全体用户的判断。可以采用循环探测方法来判断。

例如下述系统,一共有三个进程 P、Q、R,系统只有 10 个资源。

则此时只有 Q 申请资源,系统才会予以分配,其余进程申请均不会分配。

6.5.5 死锁的检测

死锁的检测对资源的分配不加限制,但系统定时运行一个 “死锁检测” 程序,判断系统内是否已出现死锁,若检测到死锁则设法加以解除。

检测方法

  • 设置两张表格来记录进程使用资源的情况。

    • 等待资源表: 记录每个被阻塞进程等待的资源
    • 占用资源表: 记录每个进程占有的资源

因此可以根据上述两张表构建出一张死锁的有向图,若 PiP_iPi​ 等待资源 rkr_krk​,且 rkr_krk​ 被进程 PjP_jPj​ 占用,则 PiP_iPi​ 和 PjP_jPj​ 具有 “等待占用关系”,记为 W(Pi,Pj)W(P_i,P_j)W(Pi​,Pj​)。

我们可以构造一个二维矩阵,运行传递必包算法,如果对角线为 1,则存在环,出现死锁。也可以用拓扑排序的方法,O(n) 判环。

for(int k = 1; k <= n; k++)for(int i = 1; i <= n; i++)for(int j = 1; j <= n; j++)b[i][j] = b[i][j] | (b[i][k] & b[k][j]);

死锁后的解决方法

  • 可以采用重新启动进程执行的办法,恢复工作应包含重启动一个或全部进程,以及从哪一点开始重启动
  • 全部卷入死锁从头开始启动,但这样的代价是相当大的
  • 在进程执行过程中定时设置校验点,从校验点开始重新执行
  • 中止一个卷入死锁的进程,以后重执行

计算机操作系统详细学习笔记(六):并发程序设计相关推荐

  1. 《计算机操作系统》学习笔记(三)---存储器管理

    一.程序的装入和链接 程序的装入分绝对装入方式和可重定位装入方式两种: 程序的链接可分为:静态链接,即程序运行之前,已将各目标模块及它们所需的函数库连接成一个完整的装配模块:装入时动态链接,即 程序装 ...

  2. 【台大郭彦甫】Matlab入门教程超详细学习笔记六:高阶绘图(附PPT链接)

    高阶绘图 前言 一.进阶二维绘图 1. 对数图 2.一图双y轴 3. 直方图 4. 条形图 5. 饼状图 6. 极坐标图 7. 阶梯图与取样图 8. 箱线图以及误差线图 9. 填充图 二.配色 1.R ...

  3. Java数据库部分(MySQL+JDBC)(一、MySQL超详细学习笔记)

    所有示例使用的数据表均为Oracle提供的SQL基础数据表(t_employees.sql dept.sql emp.sql salgrade.sql) 熟练掌握多多练习即可达到完成后端开发所需具备的 ...

  4. 《王道计算机组成原理》学习笔记和总目录导航

    <王道计算机组成原理>学习笔记和总目录导读 本篇文章是阅读和学习<王道计算机组成原理>后总结的理论知识笔记导航,专门用于遗忘后复习 下面的文章是我在学习了<王道计算机组成 ...

  5. 计算机二级Python学习笔记(七)

    上一篇:计算机二级Python学习笔记(六) 第7章 文件和数据格式化 7.1 文件的使用 文件:数据的集合和抽象,存储在辅助存储器上的一组数据序列,可以包含任何数据内容. 文件类型:文本文件(由单一 ...

  6. Java学习笔记---多线程并发

    Java学习笔记---多线程并发 (一)认识线程和进程 (二)java中实现多线程的三种手段 [1]在java中实现多线程操作有三种手段: [2]为什么更推荐使用Runnable接口? [3][补充知 ...

  7. libevent学习笔记六:libevent核心事件event

    libevent学习笔记六:libevent核心事件event 前面对reactor模式.事件处理流程.libevent源代码结构等有了高层的认识后,接下来将详细介绍libevent的核心结构even ...

  8. Ethernet/IP 学习笔记六

    Ethernet/IP 学习笔记六 EtherNet/IP defines two primary types of communications: explicit and implicit (Ta ...

  9. 吴恩达《机器学习》学习笔记六——过拟合与正则化

    吴恩达<机器学习>学习笔记六--过拟合与正则化 一. 过拟合问题 1.线性回归过拟合问题 2.逻辑回归过拟合问题 3.过拟合的解决 二. 正则化后的代价函数 1.正则化思想 2.实际使用的 ...

  10. 多线程编程学习笔记——使用并发集合(三)

    接上文 多线程编程学习笔记--使用并发集合(一) 接上文 多线程编程学习笔记--使用并发集合(二) 四.   使用ConcurrentBag创建一个可扩展的爬虫 本示例在多个独立的即可生产任务又可消费 ...

最新文章

  1. 7.matlab中使用@ + “函数名”
  2. Kafka 基本原理(8000 字小结)
  3. c++ eos智能合约开发_干货|EOS智能合约开发(一)EOS环境搭建和启动节点
  4. 分析了这么多年的福利彩票记录,原来可以用Python这么买彩票!
  5. 江门农商银行引入阿里云AnalyticDB,实现数据自助分析平台升级
  6. Android之设置横屏竖屏
  7. asp.net MVC:CheckBoxFor 绑定 nullablebool 类型
  8. Security+ 学习笔记47 事件响应方案
  9. bmi指数计算器PHP代码,BMI指数计算器
  10. 通俗易懂的Spatial Transformer Networks(STN)(一)
  11. 戴尔服务器bios设置u盘启动不了系统,戴尔电脑主板bios设置u盘启动不了怎么办...
  12. 2016年趋势科技夏令营面试题目
  13. 计算机科学学院参加些什么比赛,计算机科学学院召开2019年冬季越野赛动员大会...
  14. mac下安装pyinstaller
  15. laravel教程入门笔记
  16. 计算机系优秀团支部申报表,2016-2017学年优秀团支部评选活动圆满结束
  17. [PHP响应式营销型万能H5建站系统源码] 免费开源建站利器+可视化自由布局页面
  18. Linux之C++获取系统用户名
  19. CentOS6.5 安装wine
  20. 梯度弥散与梯度爆炸及其解决方法

热门文章

  1. JAVA字符编码系列一:Unicode,GBK,GB2312,UTF-8概念基础
  2. 网络工程师之子网划分
  3. 安徽工业大学java实验报告_安徽工业大学java实验报告.doc
  4. psm倾向得分匹配法举例_倾向得分匹配法的详细解读
  5. elasticsearch使用场景_Elasticsearch功能、适用场景及特点
  6. snmp 获取mac add table_【群晖系统】不拆机不进PE直接修改黑群晖的SN和MAC
  7. def __init__(self)是什么意思_一文搞懂什么是Python的metaclass
  8. 区间选点问题(贪心)
  9. clion使用之如何在编译运行多个程序(以cpp为例)
  10. python使用正则验证电子邮件_在Python中使用正则表达式提取电子邮件地址