lock free (中文一般叫“无锁”,一般指的都是基于CAS指令的无锁技术) 是利用处理器的一些特殊的原子指令来避免传统并行设计中对锁(lock)的使用。
众所周知,锁在解决并行过程中资源访问问题的同时可能会引入诸多新的问题,比如死锁(dead lock),另外锁的申请/释放对性能也有不小的影响,当然最大的问题还在于使用锁的代码模块通常难以进行组合。lock free的目标就是要消除锁对编程带来的不利影响。
C++大牛Andrei Alexandrescu (就是把template玩得炉火纯青的那个gg,《MODERN C++ DESIGN》的作者)的文章《Lock-Free Data Structures》是lock free方面的代表作,有兴趣的最好看一下,就当开阔思路吧。(btw, Andrei这哥们儿好像就是爱玩点bt的东西,呵呵)。
不过lock free本身也是目前各种并行解决方案中比较受争议的一种: 一来这项技术有点过于诡异,掌握起来颇有难度,不过另一方面,因为它是完全基于最基本的编程技术,所以并不依赖任何语言/平台,理论上应用面可以很广。
在并行编程方面,函数式的那些东西(比如Erlang、Haskell之类的)算得上是另起炉灶,而lock free算得上是就地解决吧。所以各种方案其实也不矛盾,都是为人民服务嘛;)
个人对lock free的观点是这项技术不应该也不会大面积地应用在实际编程中,毕竟像这种高难度的东西还是有点曲高和寡。对于技术本身反正是见仁见智,爱用就用,不用拉倒呗。不过我想无论是否在实际当中使用lock free技术,了解和研究这项技术本身都会对理解并行编程有很大的帮助。
lock free的基础是CAS (Compare And Swap)函数,它的功能可以用如下的代码描述:
template <class T> bool CAS(T* addr, T expected, T value) { if (*addr == expected) { *addr = value; return true; } return false; }
如果以前没有真正了解过lock free技术,可能会产生疑惑,这个函数对解决我们并行中的竞争问题能有什么帮助呢?如果你有这样的疑问,没问题,因为我第一次见到这个的时候也是一头雾水。不过实际上这个函数只是描述了Compare And Swap的执行过程,函数本身并不能直接被使用,只是伪代码描述而已,切记。不过现代处理器通常都实现了对应CAS功能的原子指令,比如x86汇编里面的“ CMPXCHG ”就提供了这样的功能,所以CAS的实现实际是平台相关的。文章里面给出了linux下利用asm的实现,相较之下windows上面的实现可以简单一些,因为windows提供了一族以InterlockedCompareExchange开头的API来封装具体的实现,比如其中的InterlockedCompareExchange函数声明如下:
LONG
InterlockedCompareExchange(
IN OUT PLONG Destination,
IN LONG Exchange,
IN LONG Comparand
);
这里值得注意的是函数的返回值是原始的*Destination内容,并不是像上面的C++代码描述的那样会直接返回一个布尔值指示交换操作是否真正发生。所以返回值的工作必须由我们自己来完成。显然这里不能在函数开始处对*Destination和Comparand进行比较然后用if/else这样的分支来选择返回true还是false,因为那样的话就必须有一个lock来进行保护了。我们好不容易找到个办法来避免对lock的依赖,岂能又给绕回去了?这里的标准做法是用API调用的返回值与Comparand传入参数进行比较,因为API确保返回的是一个LONG类型的值对象,这个值始终都是存在于函数的栈上面,所以即便在比较之前发生中断并且实际的*Destination内容又被其它线程修改了,也并不影响此处的比较结果,当然也不会影响CAS函数的返回值了。代码实现如下:
template<class T> bool CAS(T* addr, T expected, T fresh) { return expected == (T)InterlockedCompareExchange((LONG *)addr, (LONG)fresh, (LONG)expected); }
当然针对实际调用中T通常为指针的情况可以直接用InterlockedCompareExchangePointer来避免显示的类型转换,可以考虑再加一个偏特化的template,不过处理方法一样。
最近发现codeproject上有一篇文章分别用C++和C#实现了lock free的算法,不过很遗憾这个实现是有问题的。由此也可以说明并行程序设计特别是lock free确实不是一件容易的事情,连这样的文章都弄错了。看一下它的CAS函数实现:
bool CAS(pointer_t& dest,pointer_t& compare, pointer_t& value) { if(compare.ptr==InterlockedCompareExchangePointer((PVOID volatile*)&dest.ptr,value.ptr,compare.ptr)) { InterlockedExchange(&dest.count,value.count); return true; } return false; }
这里的问题是函数里面用了两条语句来完成对目标对象的修改,虽然两条语句本身都是atomic的,不过在它们中间仍然可能发生中断,所以这个CAS函数并没有发挥预期的作用。实际上基于CAS语句的lock free技术的本质是对于任何数据的修改并不直接修改对象本身,而是先去修改目标对象的一份拷贝(copy),然后通过实现为atomic的一次交换操作将修改后的拷贝内容赋值给目标对象。所以CAS语句通常像下面这样使用:
do { pOldData = pDest; delete pNewData; pNewData = pDest->copy(); // modify pNewData } while (!CAS(&pDest, pOldData, pNewData))
其实类似的思路也应用到解决其它问题。如果还没想到点什么的话可以去找一份C++里面智能指针(smart pointer)的代码来看看,所以这其实也是异常安全(exception safe)编码的必备武器之一。
上面的copy操作效率比较低,所以牛牛们在具体应用中想出了各种方法来减小数据copy的粒度。不过无论如何,将CAS语句实现成多条需要读写原始dest数据的操作都是不正确的。此处一种可行的做法是使用类似InterlockedCompareExchange64之流的加强版CAS API来一石二鸟,具体实现上还有很多巧妙的解法,已超出本文的范畴,就此打住。
题外话:如果之前对异常安全(exception safety)编码有所了解,可能会发现和本文谈的lock free有相似的地方。因为在异常安全里面对资源的修改最好的方式并不是直接修改目标对象本身,也是先创建/修改一份副本对象,最后通过保证没有异常抛出的swap操作来修改目标对象的内容。不了解的话可以随便找一份智能指针(smart pointer,比如boost里面的shared_ptr)的源代码l瞧瞧里面是不是用到了很多次std::swap().
lock free为的是线程访问的安全,exception safety为的是保证在抛出异常情况下数据的安全,不过解决问题的基本思路却是一样的。