【操作系统/OS笔记11】并发执行的必要性,产生的问题,原子操作,为什么引入锁机制,面包购买的类比

本次笔记内容:
9.1 背景知识
9.2 一些概念part1
9.3 一些概念part2
9.4 一些概念part3

文章目录

      • 同步互斥的背景
        • 进程间不独立存在风险
        • 进程间为什么合作?
        • 例:并发执行产生问题
      • Race Condition(竞态条件)
      • Atomic Operation(原子操作)
        • 内存读取是原子的,但未必结果确定
      • 由此引出相关基本概念
        • Critical section(临界区)
        • Mutual exclusion(互斥)
        • Dead lock(死锁)
        • Starvation(饥饿)
      • 操作系统调度的生活类比(购买面包)
        • 什么是“面包太多”问题的正确性质?
        • 在冰箱上设置一个锁和钥匙(lock&key)
          • Lock(锁)
          • Unlock(解锁)
          • Deadlock(死锁)
        • 解决面包购买办法
        • 还有更好的解决方案么
          • 之前方案获得的启示
          • 假设我们有一些锁的实现

同步互斥的背景

到目前为止学习了:

  • 多道程序设计(multi-programming):现代操作系统的重要特性;
  • 并行很有用,因为:
    • 多个并发实体:CPU(s),I/O,…,用户,…
  • 进程/线程:操作系统抽象出来用于支付多道程序设计;
  • CPU调度:实现多道程序设计的机制;
  • 调度算法:不同的策略。

将要学习:

  • 协同多道程序设计和并发问题。

进程间不独立存在风险

独立的线程:

  • 不和其他线程共享资源或状态;
  • 确定性:输入状态决定结果;
  • 可重现:能够重视启示条件,I/O;
  • 调度顺序不重要。

合并线程:

  • 在多个线程中共享状态;
  • 不确定性;
  • 不可重现。

不确定性和不可重现意味着bug可能是间歇性发生的。

进程间为什么合作?

进程/线程,计算机/设备需要合作。

  • 优点1:共享资源
    • 一台电脑,多个用户;
    • 一个银行存款余额,多台ATM机;
    • 嵌入式系统(机器人控制,手臂和手的协调)。
  • 优点2:加速
    • I/O操作和计算可以重叠;
    • 多处理器:将程序分成多个部分并执行。
  • 优点3:模块化
    • 将大程序分解成小程序:以编译为例,gcc会调用cpp,cc1,cc2,as,Id;
    • 使系统易于扩展。

例:并发执行产生问题

  • 程序可以调用函数fork()来创建一个新的进程
    • 操作系统需要分配一个新的并且唯一的进程ID;
    • 因此在内核中,这个系统调用会运行
new_pid = next_pid++;
// next_pid为共享的全局变量,赋id用
    • 将上述类c语言翻译成机器指令
LOAD next_pid Reg1   // 将new_next值赋给寄存器1
STORE Reg1 new_pid   // 将寄存器1值存到new_pid
INC Reg1             // 寄存器1值加一
STORE Reg1 next_pid  // 将寄存器1值存到next_pid

假设两个进程并发执行:

  • 如果next_pid 等于100,那么其中一个进程得到的ID应该是100,另一个进程的ID应该是101,next_pid应该增加到102。

但是,并发进程存在如下图的问题。

【操作系统/OS笔记11】并发执行的必要性,产生的问题,原子操作,为什么引入锁机制,面包购买的类比_第1张图片

进程1执行一半,发生了上下文切换。

最终进程1、进程2的PID都是100。可能在任何语句间产生上下文切换,导致结果出现不确定性并不可重复。

因此希望:

  • 无论多个线程的指令序列怎样交替执行,程序都必须正常工作:
    • 多线程程序具有不确定性和不可重现的特点;
    • 不经过专门设计,调试难度很高;
  • 不确定性要求并行程序的正确性:
    • 先思考清楚问题,把程序的行为设计清楚;
    • 切忌急于着手编写代码,碰到问题再调试。

Race Condition(竞态条件)

系统缺陷:结果依赖于并发执行或者事件的顺序/时间。

  • 不确定性
  • 不可重现

怎样避免竞态?

  • 让指令不被打断

Atomic Operation(原子操作)

原子操作是指一次不存在任何中断或者失败的执行。

  • 该执行成功结束;
  • 或者根本没有执行;
  • 并且不应该发现任何部分执行的状态。

实际上操作往往不是原子的。

  • 有些看上去是原子操作,实际上不是;
  • 连x++这样的简单语句,实际上是由3种指令构成的;
  • 有时候甚至连单条机器指令都不是原子的
    • Pipeline, super-scalar, out-of-order, page fault

内存读取是原子的,但未必结果确定

【操作系统/OS笔记11】并发执行的必要性,产生的问题,原子操作,为什么引入锁机制,面包购买的类比_第2张图片

如上图,在c语言层次上,保证原子操作,但是在程序设计上出现了问题。

由此引出相关基本概念

Critical section(临界区)

临界区是指进程中一段需要访问共享资源,并且当另一个进程处于相应代码区域时,便不会被执行的代码区域。

Mutual exclusion(互斥)

当一个进程处于临界区并访问共享资源时,没有其他进程会处于临界区并且访问任何相同的共同资源。

Dead lock(死锁)

两个或以上的进程,在相互等待完成特定任务,而最终没法将自身任务进行下去。

Starvation(饥饿)

一个可执行的进程,被调度器持续忽略,以至于虽然处于可执行状态却不被执行。

操作系统调度的生活类比(购买面包)

【操作系统/OS笔记11】并发执行的必要性,产生的问题,原子操作,为什么引入锁机制,面包购买的类比_第3张图片

如上图,出现了两次“买面包”的操作。

什么是“面包太多”问题的正确性质?

  • 最多有一个人去买面包;
  • 如果需要,有人回去买面包。

在冰箱上设置一个锁和钥匙(lock&key)

  • 去买面包之前锁住冰箱并且拿走钥匙;
  • 修复了“太多”的问题:要是有人想要果汁怎么办?
  • 可以改变“锁(lock)”的含义;
  • “锁(lock)”包含“等待(waiting)”。
Lock(锁)

在门、抽屉等物体上加上保护性装置,使得外人无法访问物体内的东西,只能等待解锁后才能访问。

Unlock(解锁)

打开保护性装置,使得可以访问之前被保护的物体类的东西。

Deadlock(死锁)

A拿到锁1,B拿到锁2,A想继续拿到锁2后再继续执行,B想继续拿到锁1后再继续执行。导致A和B谁也无法继续执行。

解决面包购买办法

【操作系统/OS笔记11】并发执行的必要性,产生的问题,原子操作,为什么引入锁机制,面包购买的类比_第4张图片

如上图,增加一种“标签”,当成一种“锁”,但是依然会产生问题。

【操作系统/OS笔记11】并发执行的必要性,产生的问题,原子操作,为什么引入锁机制,面包购买的类比_第5张图片

执行顺序如上图,简单实用note机制无法解决问题,甚至会使问题更糟。

为便签加标签怎么样?表示一下谁放的标签,如下图。

【操作系统/OS笔记11】并发执行的必要性,产生的问题,原子操作,为什么引入锁机制,面包购买的类比_第6张图片

但是如上图,有可能出现谁都不去买面包的情况。

【操作系统/OS笔记11】并发执行的必要性,产生的问题,原子操作,为什么引入锁机制,面包购买的类比_第7张图片

可以提出如上的机制,保证程序的正常执行。

还有更好的解决方案么

之前方案获得的启示

上述方法太复杂,A、B代码不同,A等待时实际在消耗CPU的时间(叫做“忙等待busy-waiting”)。

上述方案为每个线程保护了一段“临界区(critical-section)”,代码为

if (nobread) {
	buy bread;
}

如果有一个进程已经处于临界区,则其他进程不能进入临界区。互斥就是确保一个进程在临界区。

假设我们有一些锁的实现
  • Lock.Acquire() 在锁被释放前一直等待,然后获得锁;
  • Lock.Release() 解锁并唤醒任何等待中的进程;
  • 这些一定是原子操作:如果两个线程都在等待同一个锁,并且同时发现硕被释放了,那么只有一个能够获得锁。
  • 由此,面包问题得到解决:
breadlock.Acquire();  // 进入临界区
if (nobread) {
	buy bread;
}
breadlock.Release();  // 退出临界区

你可能感兴趣的:(OS)