8 同步

并发的好处

  1. 共享资源
    • 一台电脑,多个用户
    • 一个银行取款余额,多台ATM机
    • 嵌入式系统(机器人控制:手臂和手的协调)
  2. 加速
    • I/O操作和计算可以重叠
    • 多处理器 - 将程序分成多个部分并行执行
  3. 模块化
    • 将大程序分解成小程序
    • 使系统容易扩展

存在的问题

  1. 竞态条件(race condition)

  2. 并发程序共享资源时具有不确定性和不可重现性

  3. 示例:
    image-20200807100831681

    image-20200807100911101

举例

image-20200828203053324

image-20200828203457869

基本概念

  1. 临界区(Critical Section):进程中一段需要访问共享资源,且当另一进程处于相应代码区域时便不会被执行的代码区域
  2. 互斥(Mutual Exclusion):当一个进程处于临界区并访问共享资源时,没有其他进程会处于临界区并访问任何相同的共享资源
  3. 死锁(Dead Lock):两个或以上的进程,相互等待对方完成特定任务,而最终没法将自身任务进行下去
  4. 饥饿(Starvation):一个可执行进程,被调度器持续忽略,以至于虽处于可执行状态却不被执行

临界区的属性

  1. 互斥
  2. progress(前进):如果一个线程想进入临界区,那么它最终会成功
  3. 优先等待:如果一个线程 i 处于入口区,那么在 i 的请求被接收之前,其他线程进入临界区的时间是有限制的(线程 i 不会无限等待)
  4. 无忙等待(可选):如果一个进程在等待进入临界区,那么它可以在进入前被挂起(忙等会白白占用系统资源)

保护临界区的方法(锁机制)

image-20200831204544694

禁用硬件中断

  1. 过程
    • 进入临界区时,禁用中断,便没有上下文切换
    • 离开临界区时,重新启动中断
    • 硬件将中断处理延迟到中断启用之后
  2. 不足
    • 一旦中断被禁用,线程无法被停止
      • 整个系统都为你停下来
      • 其他线程可能处于饥饿状态
    • 如果临界区可以任意长怎么办?
      • 无法限制响应中断的时间长短
      • 可能对存在硬件影响
    • 不适用于多CPU的情况。多CPU情况下,禁用一个CPU的中断无法阻止另一个CPU处理中断

基于软件的解决方案

  1. Peterson算法(双进程)

    • 思考

      • image-20200829101509640
      • image-20200829101609456
      • image-20200829101650415
    • 内容

      int turn; //指示该由谁进入临界区
      boolean flag[]; //指示进程是否准备好进入临界区
      // 进程Pi的算法:
      do{
          flag[i] = TRUE;
          turn = j;
          while(flag[j] && turn == j);
          // Critical Section
          flag[i] = FALSE;
          // Remainder Section
      }while(TRUE);
      
  2. Dekker算法(双进程,较为复杂)
    image-20200829102829240

  3. Eisenberg and McGuire算法(多进程)
    image-20200829102956079

  4. Bakery算法(多进程)
    image-20200829103030103

  5. 不足

    • 复杂:需要多个进程间的共享数据项
    • 忙等浪费CPU时间
    • 需要硬件保证:Peterson算法需要LOAD(内存加载到寄存器)和STORE(寄存器存储到内存)是原子操作

基于原子操作指令

  1. 现代体系结构大多支持特殊的原子操作指令

    • 通过特殊的内存访问电路
    • 针对单处理器和多处理器
  2. 两个基本的原子操作

    • Test-and-Set 测试和置位

      • 从内存中读取值
      • 测试该值是否为1(然后返回True/False)
      • 将内存值置1
      // 语义
      boolean TestAndSet(boolean *target){
          boolean rv = *target;
          *target = TRUE;
          return rv;
      }
      
    • Exchange 交换

      • 交换内存中的两个值
      // 语义
      void Exchange(boolean *a, boolean *b){
          boolean temp = *a;
          *a = *b;
          *b = temp;
      }
      
  3. 锁的实现

    • TestAndSet

      /* 忙等待:当临界区较长时,占用较多CPU资源 */
      class Lock{
          int value = 0;
      }
      Lock::Acquire(){
          while(test-and-set(value))
              ; //忙等
      }
      Lock::Release(){
          value = 0;
      }
      
      /* 无忙等待:当临界区较短时,上下文切换代价比忙等更大 */
      class Lock{
          int value = 0;
          WaitQueue q;
      }
      Lock::Acquire(){
          while(test-and-set(value)){
          	add this TCB to q;
              schedule(); // CPU调度
          }
      }
      Lock::Release(){
          value = 0;
          remove one thread t from q;
          wakeup(t);
      }
      
    • Exchange

      int lock = 0; //共享数据
      // 线程Ti:
      int key;
      do{
          key = 1;
          while(key == 1) exchange(lock, key);
          critical section //临界区代码
          lock = 0;
          remainder section //其他代码
      }
      
  4. 优点

    • 适用于单处理器或共享内存的多处理器中的任意数量的进程
    • 简单且容易证明
    • 支持多临界区
  5. 缺点

    • 忙等消耗CPU资源
    • 由于锁分配的随机性,当进程离开临界区,且多个进程等待临界区时,可能导致饥饿
    • 死锁:如果一个低优先级进程进入临界区,而一个高优先级进程也有需求,高优先级进程获得处理器并等待临界区 ⇒ 可通过优先级反转解决
posted @ 2022-02-16 14:12  DreamEagle  阅读(37)  评论(0编辑  收藏  举报