基础

  • 竞态条件Race condition: 多个进程并发访问和操作同一数据且执行结果与访问发生的特定顺序有关。
  • 临界区问题
    • 特征:没有两个进程可以同时在临界区内执行。
    • 要求:
      • 互斥Mutual Exclusion
      • 前进Progress: 如果当前没有进程在临界区内执行,且有进程需要进入临界区,需要选择,且这种选择不能被无限推迟。
      • 有限等待Bounded Waiting
  • Peterson算法: TBD

硬件同步

  • 一般来说,任何临界区问题都需要一个简单工具:锁
  • 硬件上的解决方案:
    • 单处理器环境,可以通过简单的禁中断,即修改共享变量需要禁止中断出现。这种方式一般用在非抢占内核上。
    • 特殊硬件指令以允许能原子地(不可中断地)检查和修改字的内容和交换两个字的内容
      • 比如用TestAndSet原子指令来实现互斥:
        do {
          // TestAndSet will automatically set lock=True and return the original value of lock
          while(TestAndSet(&lock));   // do nothing
          // critical section
          lock = False;
          // remainder section
        } while(True);
      • 比如用Swap原子指令实现互斥:
        do {
          flag = True;
          while (flag) Swap(&flag, &lock); // do nothing
          // critical section
          flag = False;
          // remainder section
        } while(True)
      • 但上述两个方法的问题是:没有解决有限等待问题

信号量

  • 显然,上述基于硬件的解决方案,对于应用程序员而言,使用比较复杂。为了解决这个困难,可以使用称为信号量的同步工具
  • what is 信号量(semaphore)?
    • 一个整数变量
    • 除了初始化外,只能通过两个标准原子操作来访问:(底层是硬件层面的原子操作)
      • wait()
        wait(S) {
          while (S <= 0);  // no op: busy waiting
          S--;
        }
      • signal()
        signal(S) {
          S++;
        }

信号量用法

  • 信号量分类:
    • 二元信号量称为互斥锁,因为它们可以提供互斥
      do {
        wait();
        // critical section
        signal();
        // remainder section
      } while(True)
    • 计数信号量:可以初始化为可用资源的数量,用以控制访问具有若干个实例的某种资源。
  • 信号量也可以用来解决各种同步问题:
    • 比如,有两个并发进程,P2的语句S2 希望在 P1的语句 S1执行之后才执行。
      // P1
      S1;
      signal();
      
      
      // P2
      wait();
      s2;

信号量实现

  • 忙等待busy waiting:
    • 这种类型的信号量也称为自旋锁spinlock
    • 优点:不进行上下文切换,通常上下文切换相当费时。比较适合锁占用时间较短的场景。
    • 缺点:浪费CPU时钟。
    • 常用于多处理器系统中。
  • 阻塞:阻塞操作将一个进程放入到与信号量相关的等待队列中,并将该进程的状态切换成等待。接着,控制转到CPU调度程序,以选择另一个进程来执行。
    • 实现方式:每个信号量都有一个整型值和一个进程链表
      wait(semaphore S) {
        S.value--;
        if (S.value < 0) {
          add this process to S.list;
          block;
        }
      }
      
      signal(semaphore S) {
        S.value++;
        if (S.value <= 0) {
          remove a process P from s.list;
          wakeUp(P);  // This func will switch P from block to runnable
        }
      }
    • block() 操作挂起调用它的进程。wakeUp(P)重新启动阻塞进程P的执行。这两个操作都是由os作为基本系统调用来提供的。
  • 信号量的关键之处就在于它们原子地执行,必须确保没有两个进程能同时对同一信号量执行操作wait()/signal()。如何实现原子执行呢?这又属于临界区问题了:
    • 在单处理器情况下,可以通过简单地禁止中断
    • 在多处理器环境下,必须禁止每个处理器的中断。但是这会严重影响性能。

经典同步问题

有限缓冲问题

  • 信号量mutex提供对缓冲池访问的互斥要求,二元。
  • 信号量empty 和信号量full 分别表示空缓冲项和满缓冲项的个数。分别初始化为n,0。
  • // producer
    do {
      // produce an item
      wait(empty);  // wait until there exists empty slot
      
      wait(mutex); // mutex access for buffer
      // add the item to buffer
      signal(mutex);
      
      signal(full);
      
    } while(True)
    
    
    // consumer
    do {
      wait(full);  // wait until there exists 
      
      wait(mutex);   // require exclusive access for buffer
      // remove an item from buffer
      signal(mutex);
      signal(empty);
      
      // do real consume
    } while(True)

读者-写者问题

  • 分类:在某种程度上是根据读者/写者之间的优先级来分:
    • 第一读者-写者问题:要求没有读者需要等待除非已有一个写者已获得允许以使用共享数据库。[其实这种方式写者的优先级还是挺低的,要等待连续的读者都洗完]
    • 第二读者-写者问题:一旦写者就绪,那么写者会尽可能快的执行写操作。[这种方式下,只要有写者等待访问对象,就不会有新的读者开始读操作]
  • 第一读者-写者问题:
    • readcnt统计当前读者个数,只要当前已有读者,新readers就可以持续进入 [readcnt只是一个简单的变量,并不是信号量]
    • 需要mutex互斥锁才保证更新readcnt时的互斥
    • 互斥量wrt供写者作为互斥信号量。
    • // reader
      do {
        wait(mutex);   // make sure exclusive access for readcnt
        readcnt++;
        if (readcnt == 1) wait(wrt); // no readers reading, so wait wrt
        signal(mutex);
        // reading
        wait(mutex);
        readcnt--;
        if (readcnt == 0) signal(wrt);
        signal(mutex);
      } while(True)
      
      
      // writer
      do {
       wait(wrt);   // wait until no readers reading
       // writing,,,
       signal(wrt);
      } while(True)
  •  读写锁适用场景:多读少写。因为读写锁简历开销通常大于信号量or互斥锁,而这一开销可以通过允许多个读者来增加并发度的方法来弥补。