深入理解相互排斥锁的实现

在实际的软件编程中,常常会遇到资源的争用,比方以下的样例:

[cpp] view plaincopyprint?

  1. class Counter
  2. {
  3. private:
  4. int value;
  5. public:
  6. Counter(int c) { value = c; }
  7. int GetAndIncrement()
  8. {
  9. int temp = value;
    //进入危急区
  10. value = temp +1; //离开危急区
  11. return value;
  12. }
  13. }
class Counter
{
     private:
        int value;
     public:
        Counter(int c) { value = c; }
        int GetAndIncrement()
        {
           int temp = value;      //进入危急区
            value = temp +1;       //离开危急区
            return value;
        }
 }

这样的实如今单线程系统中可以正常工作,可是在多线程系统则有可能出错。比方有2个线程,初始状态value=0。第一个线程执行完第9行,这时temp=0。突然一个中断来了,切换到第二个线程执行了,第二个线程执行完第9行也是temp=0,然后执行第10行赋值value=1。然后回到第一个线程继续执行第10行对value进行写覆盖,结果value=1.而正确的情况应该是value=2了。

为什么会产生这种情况呢?这时由于两个线程同一时候对一个资源value进行争用产生了冲突。为了避免上述情况,我们能够将这两行置入临界区内:某个时刻内仅能被一个线程运行的代码段。从而实现相互排斥。对Counter类的添加对临界区的相互排斥訪问:

[cpp] view plaincopyprint?

  1. class Counter
  2. {
  3. private:
  4. int value;
  5. lock lock;
  6. public:
  7. Counter(int c) { value = c; }
  8. int GetAndIncrement()
  9. {
  10. lock.lock();//获取锁
  11. int temp = value;
    //进入临界区
  12. value = temp +1; //离开临界区
  13. lock.unlock();//释放锁
  14. return value;
  15. }
  16. }
 class Counter
 {
    private:
      int value;
      lock lock;
    public:
      Counter(int c) { value = c; }
      int GetAndIncrement()
      {
        lock.lock();//获取锁
         int temp = value; //进入临界区
         value = temp +1; //离开临界区
         lock.unlock();//释放锁
         return value;
     }
 }

通过在程序中为了使用Lock域来保证对象的相互排斥特性,必须对称的调用lock()和unlock()。须要满足例如以下条件:

1. 一个临界区之和一个lock对关联。

2. 线程进入临界区前调用lock()。

3. 线程离开临界区后调用unlock().

编程的框架例如以下:

lock()

临界区

unlock()

打个不是十分妥帖的比喻,就像是有一个仓库资源,可是有多个人想去仓库做点事情。这时候仓库仅仅须要一把锁(锁多了纯粹是浪费^_^),初始状态仓库上的锁是打开的。每一个人进去之前先把锁锁住(避免别的人进来),然后自己在仓库里捣弄,离开时再把仓库的锁打开,让别人能够进来。

接下来更加深入的是怎样实现相互排斥锁呢?也就是lock()和unlock()方法。

[cpp] view plaincopyprint?

  1. class Lock
  2. {
  3. public:
  4. virtual void lock() = 0;
    //进入临界区前
  5. virtual void unlock() = 0;
    //离开临界区后
  6. }
class Lock
{
   public:
     virtual void lock() = 0; //进入临界区前
      virtual void unlock() = 0; //离开临界区后
}

相互排斥锁须要满足三个条件:

相互排斥 不同线程的临界区没有重叠

无死锁 假设一个线程正在尝试获得一个锁,那么总会成功地获得这个锁。若线程A调用lock()可是无法获得锁,则一定存在其它线程正在无穷次地运行临界区。

无饥饿 每个试图获得锁的线程终于都能成功。

首先看双线程的相互排斥,首先从两个存在不足(假设大家能不看后面的分析也能知道哪里不足就更厉害了^_^),但十分有趣的锁算法说起:

LockOne类

这个类有一个标志数组flag,继续来个比喻,这个flag就相当于一个旗帜。LockOne类遵循这种协议:

1. 假设线程想进入临界区,首先把自己的旗帜升起来(flag对应位置1),表示感兴趣。然后等对方的旗帜降下来就能够进入临界区了。

2. 假设线程离开临界区,则把自己的旗帜降下来。

[cpp] view plaincopyprint?

  1. class LockOne: public Lock
  2. {
  3. private:
  4. bool flag[2];
  5. public:
  6. void lock()
  7. {
  8. int i = ThreadID.get();
  9. int j = 1-i;
  10. flag[i] = true;
  11. while(flag[j]);
  12. }
  13. void unlock()
  14. {
  15. int i = ThreadID.get();
  16. flag[i] = false;
  17. }
  class LockOne: public Lock
  {
       private:
         bool flag[2];
       public:
         void lock()
         {
            int i = ThreadID.get();
            int j = 1-i;
            flag[i] = true;
            while(flag[j]);
         }
         void unlock()
         {
            int i = ThreadID.get();
            flag[i] = false;
         }
 }

LockOne类的协议看起来挺朴实的,可是存在一个问题:当两个线程都把旗帜升起来,然后等待对方的旗帜降下来就会出现死锁的状态(两个线程都在那傻乎乎的等待对方的旗帜降下来,直到天荒地老:))

LockTwo类

观察LockOne类存在的问题,就是在两个线程同一时候升起旗帜的时候,须要有一个线程妥协吧,这样就须要指定一个牺牲品,因此LockTwo类横空出世。

[cpp] view plaincopyprint?

  1. class LockTwo: public Lock
  2. {
  3. private:
  4. int victim;
  5. public:
  6. void lock()
  7. {
  8. int i = ThreadID.get();
  9. victim = i; //让别人先走,临时牺牲自己
  10. while(victim == i);
  11. }
  12. void unlock(){]
  13. }
 class LockTwo: public Lock
 {
    private:
      int victim;
    public:
      void lock()
      {
         int i = ThreadID.get();
         victim = i;                  //让别人先走,临时牺牲自己
         while(victim == i);
      }
      void unlock(){]
 }

当两个线程进行竞争的时候,总有一个牺牲品(较晚对victim赋值的线程),因此能够避免死锁。可是,当没有竞争的时候就杯具了,假设仅仅有一个线程想进入临界区,那么牺牲品一直是自己,直到等待别人来替换自己才行。

Perterson锁

通过上面两个类能够发现,LockOne类适合没有竞争的场景,LockTwo类适合有竞争的场景。那么将LockOne类和LockTwo类结合起来,就能够构造出一种非常好的锁算法。该算法无疑是最简洁、最完美的双线程相互排斥算法,依照其发明者的名字被命名为“Peterson算法”。

[cpp] view plaincopyprint?

  1. class Peterson: public Lock
  2. {
  3. private:
  4. bool flag[2];
  5. int victim;
  6. public:
  7. void lock()
  8. {
  9. int i = ThreadID.get();
  10. int j = 1-i;
  11. flag[i] = true;
  12. victim = i;
  13. while(flag[j] && victim==i);
  14. }
  15. void unlock()
  16. {
  17. int i = ThreadID.get();
  18. flag[i] = false;
  19. }
  20. }
class Peterson: public Lock
{
   private:
     bool flag[2];
     int victim;
   public:
     void lock()
     {
        int i = ThreadID.get();
        int j = 1-i;
        flag[i] = true;
        victim = i;
        while(flag[j] && victim==i);
      }
      void unlock()
      {
         int i = ThreadID.get();
         flag[i] = false;
      }
}

Perterson锁是满足相互排斥特性的。通过反证法来说明,假设两个线程都想进入临界区,可是都成功进入了。由于两个线程都想进入,则说明flag相应位均为1,然后由于都能lock()成功,说明victim均不是自己。这和victim是当中之中的一个矛盾。

可是,实际中线程不可能仅仅有2个,接下来须要看看支持n线程的相互排斥协议。

Barkey锁

有一种协议称为Bakery锁,是一种最简单也最为人们锁熟知的n线程锁算法。以下看看究竟是神马情况。思想非常easy,还是打个简单的比喻来说明器协议:

1. 每一个线程想进入临界区之前都会升起自己的旗帜,并得到一个序号。然后升起旗帜的线程中序号最小的线程才干进入临界区。

2. 每一个线程离开临界区的时候降下自己的旗帜。

[cpp] view plaincopyprint?

  1. class Bakery: public Lock
  2. {
  3. private:
  4. bool flag[];
  5. Label label[];
  6. public:
  7. Bakery (int n)
  8. {
  9. flag = new
    bool[n];
  10. label = new Label[n];
  11. for(int i=0; i<n; i++)
  12. {
  13. flag[i] = flase;
  14. label[i] = 0;
  15. }
  16. void Lock()
  17. {
  18. int i = ThreadID.get();
  19. flag[i] = true;
  20. label[i] = max(label[0], ..., label[n-1]) +1;
  21. while((exist k!=i)(flag[k] && (label[k],k)<<(label[i], i))
  22. }
  23. void unlock()
  24. {
  25. flag[TheadID.get()] = false;
  26. }
  27. }
  28. }
class Bakery: public Lock
{
  private:
    bool flag[];
    Label label[];
  public:
    Bakery (int n)
    {
      flag = new bool[n];
      label = new Label[n];
      for(int i=0; i<n; i++)
      {
        flag[i] = flase;
        label[i]  =  0;
      }
      void Lock()
      {
         int i = ThreadID.get();
         flag[i] = true;
         label[i] = max(label[0], ..., label[n-1]) +1;
         while((exist k!=i)(flag[k] && (label[k],k)<<(label[i], i))
       }
       void unlock()
       {
          flag[TheadID.get()] = false;
       }
     }
}

首先,Barkey算法是无死锁的。由于正在等待的线程中(类似于全部升起旗帜flag的线程中),必然存在一个最小的序号label。该线程能够进入临界区。

其次,Barkey算法是先来先服务的。因此先来的线程,分到的label比較小。

最后,Barkey算法是相互排斥的。假设两个线程同一时候位于临界区,则两个线程都已经升起旗帜,同一时候label都是最小的,矛盾。

非常重要的点是要实现一个n线程的相互排斥锁,必须至少使用n个存储单元。由于若此刻有某个线程正在临界区内,而锁的状态却与一个没有线程在临界区或正在临界区的全局状态相符,则状态不一致。即每一个线程共同拥有2个状态,则n个线程共同拥有2^n个状态,共须要n个存储器记录全局状态。

时间: 2024-08-06 11:55:03

深入理解相互排斥锁的实现的相关文章

RAII手法封装相互排斥锁

CriticalSectionWrapper是一个接口类 class CriticalSectionWrapper { public: // Factory method, constructor disabled static CriticalSectionWrapper* CreateCriticalSection(); virtual ~CriticalSectionWrapper() {} // Tries to grab lock, beginning of a critical se

线程同步与相互排斥:相互排斥锁

为什么须要相互排斥锁? 在多任务操作系统中,同一时候执行的多个任务可能都须要使用同一种资源.这个过程有点类似于,公司部门里.我在使用着打印机打印东西的同一时候(还没有打印完).别人刚好也在此刻使用打印机打印东西,假设不做不论什么处理的话,打印出来的东西肯定是错乱的. 以下我们用程序模拟一下这个过程.线程一须要打印" hello ",线程二须要打印" world ",不加不论什么处理的话.打印出来的内容会错乱: [cpp] view plaincopy #includ

Linux 同步方法剖析--内核原子,自旋锁和相互排斥锁

在学习 Linux® 的过程中,您或许接触过并发(concurrency).临界段(critical section)和锁定,可是怎样在内核中使用这些概念呢?本文讨论了 2.6 版内核中可用的锁定机制,包含原子运算符(atomic operator).自旋锁(spinlock).读/写锁(reader/writer lock)和内核信号量(kernel semaphore). 本文还探讨了每种机制最适合应用到哪些地方.以构建安全高效的内核代码. 本文讨论了 Linux 内核中可用的大量同步或锁定

自旋锁与相互排斥锁之抉择

自旋锁和相互排斥锁是多线程编程中的两个重要概念.他们都能用来锁定一些共享资源,以阻止影响数据一致性的并发訪问.可是他们之间确实存在差别,那么这些差别是什么? 1    理论 理论上,当一个线程试图获取一个被锁定的相互排斥锁时,该操作会失败然后该线程会进入睡眠,这样就能立即让还有一个线程执行.当持有相互排斥锁的线程释放该锁之后,进入睡眠状态的线程就会被唤醒.可是,当一个线程试图获取一个自旋锁而没有成功时,该线程会不断地重试,直到终于成功为止:因此该线程不会将执行权交到其它线程手中(当然,一旦当前线

Linux多线程同步之相互排斥量和条件变量

1. 什么是相互排斥量 相互排斥量从本质上说是一把锁,在訪问共享资源前对相互排斥量进行加锁,在訪问完毕后释放相互排斥量上的锁. 对相互排斥量进行加锁以后,不论什么其它试图再次对相互排斥量加锁的线程将会被堵塞直到当前线程释放该相互排斥锁.假设释放相互排斥锁时有多个线程堵塞,所以在该相互排斥锁上的堵塞线程都会变成可进行状态.第一个变成执行状态的线程能够对相互排斥量加锁.其它线程在次被堵塞,等待下次执行状态. pthread_mutex_t 就是POSIX对于mutex的实现. 函数名 參数 说明 p

Linux同步与相互排斥应用(零):基础概念

[版权声明:尊重原创,转载请保留出处:blog.csdn.net/shallnet 或 .../gentleliu,文章仅供学习交流,请勿用于商业用途] 当操作系统进入多道批处理系统时代以后.一个系统中就存在多个任务,每一个任务都依照一定的算法进行调度来使用内存.cpu等共享资源. 当当中一个任务等待其它资源时,该任务能够临时睡眠,操作系统调度另外任务继续运行额,这样能够使系统资源得到最大化利用.而无需像曾经单道批处理系统那样仅仅有当一个任务完毕之后才运行下一个任务. 可是由此也引入了多任务并发

进程间通信(一):竞争条件与相互排斥方案

进程间通信(一):进程之间的冲突与处理方式 --<现代操作系统第二章第三节> 1.问题的提出 我们想象一个假脱机打印程序.当一个进程须要打印一个文件时,它会将该文件放在一个假脱机文件夹下.还有一个进程负责周期性地检查是否有文件须要被打印,如果有就打印并将其在文件夹中删除. 简单设想.脱机文件夹中有非常多槽位,每一个槽位中存放文件名称,如果它们有两个共享的变量:out,指向下一个要被打印的文件:in,指向下一个空暇的槽位. 如图,下一个被打印的应该是4号槽,下一个入队的应该是7号槽. 如今,如果

Android多线程研究(3)——线程同步和相互排斥及死锁

为什么会有线程同步的概念呢?为什么要同步?什么是线程同步?先看一段代码: package com.maso.test; public class ThreadTest2 implements Runnable{ private TestObj testObj = new TestObj(); public static void main(String[] args) { ThreadTest2 tt = new ThreadTest2(); Thread t1 = new Thread(tt,

Linux线程相互排斥量--进程共享属性

多线程中.在相互排斥量和 读写锁的 属性中.都有一个叫 进程共享属性 . 对于相互排斥量,查询和设置这个属性的方法为: pthread_mutexattr_getpshared pthread_mutexattr_setpshared 我一開始不理解什么是 进程共享属性. 看了man中的说明例如以下 The pthread_mutexattr_getpshared() function shall obtain the value of the process-shared attribute