这几天在研究无锁编程的一些事情。
这里是内核kfifo(无锁循环队列,主要用于单一读者与单一写者)代码介绍:http://blog.csdn.net/linyt/article/details/5764312 代码精妙处原文作者已经解释得十分清楚了,然而,作者略过了这三个函数的介绍 :
smp_rmb()
smp_wmb()
smp_mb()
这几个函数用于同步CPU各个核的cache line。是不是太专业了一点,其实我也不太懂硬件方面的东西,但总归要知道cpu是多核的,每个核有自己的cache,读写内存都先通过cache。然后呢,内存只有一个,核有多个,也就是说,同一份数据在内存只有一份,但却可能同时存在于多个cache line中。
现在转过头来说另一个事实,所有无锁算法都依赖于一些原子操作,例如compare_and_set, atomic_add, atomic_sub之类的(这些操作和cpu本身提供的原子指令并不一样,原子指令例如cmpxchg只是确保在完成这个操作之前进程不会被抢占)。这些操作可以执行的一个前提是——独占。假如一个变量a同时存在于核1和核2的cache line中,那么当核1想要进行atomic_add的时候必须先独占这个变量a,也就是告诉核2变量a在你的cache line已经无效了,以后想要操作a的时候到我这里来取最新的值。是不是有点像锁呢?恩,是挺像的,这正是本文标题想要表达的意思。(如果变量a不在cache line中呢?那么核1要锁住的将不是cache line,而是内存总线,一个消耗更大的操作)
然后上面的三个函数,被称为内存屏障,用于cpu各个核的cache line通信,至于实际上通信的过程是怎样的,这篇写得不能再好(墙外):http://sstompkins.wordpress.com/2011/04/12/why-memory-barrier%EF%BC%9F/
同时,这三个函数只存在于内核源代码中,要在自己的代码中达到同样的效果,可以使用更上层的api,例如gcc 4.2以上的版本都内置提供__sync_synchronize()这类的函数,效果差不多。(或者你自己去抄内核的实现)
这里来盗窃一下内核的源代码,修改一下变成一个简单的“无锁”循环队列:
#define QUEUE_SIZE 1024 typedef unsigned int ElementType; struct kfifo { ElementType data[ QUEUE_SIZE ]; unsigned int in; unsigned int out; }; int kfifo_put( struct kfifo *fifo, ElementType element ) { if ( QUEUE_SIZE - fifo->in + fifo->out == 0 ) return 0; __sync_synchronize(); //确保取出的out是最新的(它是这么说的,但极度怀疑不需要) unsigned int index = fifo->in & (QUEUE_SIZE - 1); fifo->data[ index ] = element; __sync_synchronize(); //确保先写入数据再更新in fifo->in++; return 1; } int kfifo_get( struct kfifo *fifo, ElementType *element ) { if ( fifo->in - fifo->out == 0 ) return 0; unsigned int index = fifo->out & (QUEUE_SIZE - 1); __sync_synchronize(); //确保读出的in是最新的(同上) *element = fifo->data[ index ]; __sync_synchronize(); //确保先读取数据再更新out fifo->out++; return 1; }
嘛,差不多了。