无锁技术目前使用的非常多,基本上新的内存分配库的核心都是使用无锁技术实现的,设计思路非常简单,就是减少并发冲突, 提升效率。
具体来说,一个是使用使用thread_local, 那么每个线程有自己独立的操作对象,那么这种存取方式就是非常高效的,完全没有锁, 代码实现非常简单,定义一下thread local就可以用了。
其次,就是per CPU的方式,就是说数据对象按CPU分组,那么这种方式的锁冲突也是非常小(例如执行中线程被切换情况),虽然代码中使用了锁,但是实质上几乎等同于无锁,因为几乎没有锁冲突,效率也非常高,接近无锁的性能,设计代码也非常简单,获得当前CPU编号,作为vector的offset, 获取按CPU分组的对象 。
第三, 就是lock_free技术,这种主要是cas指令来实现,有很多的开源库,质量也参差不齐。这块的各种技术介绍的文章非常多。lock free架构主要适合并发冲突不大的场合。
第四, 就是wait_free技术,这种是目前性能最高的,主要是内存屏障fence指令和atomic等来实现,现在也有很多的开源库和代码,比较常见的一种数据结构是ring buffer架构,技术介绍的文章也非常多。很多项目组都有自研库,不会轻易使用开源的库,避免掉坑里。以我们使用的自研wait free模板库为例,代码行数有数千行之多,很多代码用于处理各种 corner case,避免掉坑里,外围代码比较多,核心代码都是大同小异,都是一个架构的理论体系出来的各种变种,代码量不多。有一些 wait_free 开源库代码总量太低,尤其是一两百行源码的库,可以用来学习,是玩具级别的库。一个参考:boost:lock free:spsc queue 库已经算是相对细致的开源库了,有大约两千行源码,也没有彻底脱离玩具的范畴。
========================================================
补充说明:
主要谈内存池的设计中的无锁,
1)使用使用thread_local,是内存申请时, 每个线程内部有一个小池子,存放到thread_local中,多数情况下,可以直接从这个线程内部的小池子分配内存, 就没有冲突了。
2)per CPU的方式,是用于 1)这个小池子的管理内存,从per cpu的池子中分配,减少了冲突。
1)和2)都是简化冲突的做法,实现的成本很低,可以几行代码达到比较好的效果。
4)wait_free技术 是主要用于跨线程释放的队列数据结构,A线程申请的内存,生成class X, 例如:shared_ptr, 最后是B线程释放这个对象,free时B线程检测指针对应的cookie 数据获取thread id, 判断是跨线程的释放,就传给A线程的wait free queue,是以生产者的方式push进队列。 A线程作为消费者可以择机pop出队列(下次malloc/free执行时)释放指针。这种方式性能很高, 利用了wait free 架构的高并发的优势。
数据或逻辑无法/难以分段的情况,比如对同一临界对象互斥访问,除了锁之外,常用的就只有lock_free和RCU(Read copy,update) 2种方式,其中RCU方式在Linux内核中非常常见,但是这种RCU代码普通程序员基本写不对,所以,用的人不多,一个难点是指针的释放,有一堆的论文,讲了各种处理方案,类似Hazard Pointer之类的,都很冗长,一个难点是限制同时只能有一个线程处理写流程(可以同时并发多个线程的读流程),类似于乐观锁,能够retry处理, 能高效的写对这部分代码的不多,当年Linux内核最早的一批RCU代码几乎都是一个人完成的。无锁的代码,能够独立写对的人其实也不多,早年的很多论文中的代码最终都被证明是有漏洞的。
========================================================================
补充一点注意事项:
写无锁代码时,x86_64下也要用各种内存屏障fence指令,现在的 Intel CPU 内部已经是在乱序执行了,最高同时并发10条相邻指令,在x86下的 _mm_lfence 和 _mm_sfence 不是空指令,是有用的指令,大约有数ns的执行周期, _mm_mfence 的执行周期大约是(lfence+sfence)*2, 开销更大。
作者:郭忠明