实现无锁的栈与队列(1)

为了实现一个快速无锁的 logging 模块, 这几天花了不少时间去了解怎样实现一些无锁的操作及与之相对应的数据结构。对多线程场景下的无锁操作的研究一直是个热点,理想中的无锁操作,它应能天然地避开有锁操作的一些缺陷,比如:

    1)减少线程切换,能够相对快速高效地读写(不使用 mutex, semaphore)

    2)避免死锁的可能,任何操作都应能在有限的等待时间内完成,

这些优点是很有吸引力的,它们从根本上绕开了有锁操作可能引起的令人头疼的同步死锁问题,那么它会是我们的救世主吗? 要了解无锁的数据结构,我们不妨先来回顾一下常规的数据结构是怎么写。

 1  // 一般的栈。
 2  
 3  typedef ELEM int;
 4  #define MAX (2048)
 5  
 6  static ELEM Stack[MAX];
 7  static int top = 0;
 8  
 9  bool Push(const ELEM& val)
10  {
11      if (top >= MAX) return false;
12  
13      Stack[top] = val;
14      ++top;
15      return true;
16  }
17  
18  
19  bool Pop(ELEM& val)
20  {
21      if (top == 0) return false;
22  
23      --top;
24      val = Stack[top];
25      
26      return true;
27  }

这样的栈在单线程场合下是常见的,也很简洁明了,但它却不适用于多线程的场合,试想一下,如果两个线程,线程 a, 线程 b, 同一时间对同一个栈进行 Push 操作,参考上面的代码,假设此时 top = 0, 如果线程 a 在执行到第13 行时停了下来,切换到线程 b 进行 Push,线程 b 执行完 13 行,但没有执行 14 行的时候,这时 Stack[top] 中已经插入了线程 b 要插入的值,但 top 还没更新,如果这时线程 b 不幸又被切换了出去,换到线程 a 继续执行,那么线程 a 又会在同样一个位置 top = 0 的地方插入,从而破坏了线程b的操作!

我们可以观察到,上面的代码在多线程下之所以不安全,是因为 Stack 被多个线程同时修改,但各个线程又没有对关键的变量在访问顺序上作保护。对此,我们可以引入一些同步的机制来修改它,使得它能在多线程的场合里是操作安全的。   

 1 //带锁的栈。
 2  
 3  typedef ELEM int;
 4  #define MAX (2048)
 5  
 6  static ELEM Stack[MAX];
 7  static int top = 0;
 8  
 9  static Mutex mutex;
10  
11  bool Push(ELEM val)
12  {
13      if (top >= MAX) return false;
14  
15      Lock(&mutex);
16 
17      Stack[top] = val;
18      ++top;
19      
20      Unlock(&mutex);
21 
22      return true;
23  }
24  
25  
26  bool Pop(ELEM& val)
27  {
28      if (top == 0) return false;
29  
30      Lock(&mutex);
31 
32      --top;
33      val = Stack[top];
34        
35      Unlock(&mutex);
36      return true;
37  }

上面的代码就是我们常说的有锁操作了,mutex 保证了各个线程对公共变量的访问是安全的,各个线程在同时对 Stack 进行操作时,需要先抢占 mutex,抢到就可以对 stack 进行操作,没抢到就先等着。这里付出了些代价,但保证了操作的安全可靠性。那么这些保护是有必要的吗?再观察一下前面的代码,多个线程有可能,有需要同时修改的变量就一个而已: top. 只要我们参保证 top 在多线程的环境里能够安全地被修改,那对整个 stack 的修改也都是安全的。事情看起来,好像比较简单。要保证对 top 变量的原子操作,我们需要 cpu 提供一些特殊的支持,来保证我们在对某些内存进行修改时,不会被线程所中断,它要么就完成,要么就不完成,而不会在完成到一半时,被别的线程中断。在 intel 平台上,从 80486 开始,CMPXCHG 汇编指令可以帮助我们完全这件事情,这就是我们通常所说 CAS 操作的基础。

下面我们尝试用 cas 来写一个无锁的 stack.  

 1 // 无锁的栈。
 2  
 3  typedef ELEM int;
 4  #define MAX (2048)
 5  
 6  static ELEM Stack[MAX];
 7  static int top = 0;
 8  
 9  bool Push(ELEM val)
10  {
11      int old_top;
12      
13      do
14      {
15         old_top = top;
16         if (old_top >= MAX) return false;
17         
18         if (cas(&top, old_top, old_top + 1)) 
19             break;
20 
21       }while(1);
22   
23      Stack[old_top] = val;
24      
25      return true;
26  }
27  
28  
29  bool Pop(ELEM& val)
30  {
31      int old_top;
32      do
33      {
34          old_top = top;
35      
36          if (old_top == 0) return false;
37          
38          val = Stack[old_top - 1];
39 
40          if (cas(&top, old_top, old_top - 1))
41               break;
42 
43       } while(1);
44 
45 
46      return true;
47  }

上面的实现乍看起来很美好, 它会是我们想要的东西吗?        

posted on 2013-06-30 23:59  twoon  阅读(5020)  评论(8编辑  收藏  举报