参考文章
- Lock-Free Data Structures, Andrei Alexandrescu, 2004
- Lock-Free Data Structures with Hazard Pointers, Andrei Alexandrescu, Maged Michael, 2004
- Implementing Lock-Free Queues, John D, Valois, 1994
- Yet another implementation of a lock-free circular array queue, Faustino Frechilla, 2011
Yet another implementation of a lock-free circular array queue, Faustino Frechilla, 2011
(本地无图版)
Cache Trashing: 切换thread的开销:1.上下文Save&Load;2.Cache被其他线程覆盖;
传统mutex+cond方法,对IsEmpty和Push和Pop都加锁;
文献里的方法,用的链表和CAS lock-free,会有ABA问题;
解决ABA问题,一种方法是自己实现堆内存管理,申到的内存块有计数;一种方法是不是用堆内存;
本文用循环数组,实现有size上限的lock-free队列,不惧怕ABA问题;
用了3个index: readIndex, writeIndex, maxReadIndex; Push操作先把writeIndex加1,再在原writeIndex位置写入数据,再将maxReadIndex加1;类似2阶段提交;
之所以在Write的第2阶段的循环里进行了类似Sleep(0)的让出CPU,是因为当多个Push线程要在这一个CPU核上执行时,自己堵着执行顺序靠前的线程,会造成更多时间开销的浪费;
由于第2阶段的提交需要按第一阶段的顺序来走,多了等待的可能性,所以和glib的mutex阻塞版本相比,本lock-free版本更适合只有1个线程写入,或者1个线程忙碌写入的情况下,比mutex版本快1倍;2个忙写线程就比mutex慢个10%了,3个忙些线程比mutex版本慢1倍;
lock-free的难点,在于:1. 要考虑任何时刻当前线程被换出打断,会不会造成数据一致性的错误;2.要考虑任何时刻当前线程挂掉了,其他线程能不能不受影响继续正确的逻辑;
Lock-Free Data Structures, Andrei Alexandrescu, 2004
加锁的缺点:1.在临界区进行IO操作会让别的线程更久的等待;2.多个锁时,容易造成死锁;
本篇论文没有考虑Memory Barrier的问题,假设都是按顺序访问的;
wait-free: 任意线程都可以在有限步数內完成操作;lock-free:所有线程里肯定有线程能往前走一步,不保证任意线程都能往前走;
live-lock: 2个人狭路相逢,积极躲对方但总是和对方躲到一处,2个线程都没block,但就是过不去;夫妻吃饭互相礼让勺子;
wait-free和lock-free的好处:1. 任意线程突然挂掉,不影响其他线程的进度继续;2.类似malloc这样加了锁后被中断,则在中断程序里不敢再调用malloc了;3.加锁的话,低优先级线程占有了锁,高优先级线程也去要,产生优先级反转,即OS必须先执行低优先级线程;
实现“写少读多”map(系统要有GC支持): 1. 读直接读;2.写则把老map复制到新map, 往新map写入,最后CAS(if pMap==pOld then pMap<=pNew,return true else return flase); 3. 老map必须在没有别的线程正在读,才能自动被GC回收;
如果没有GC支持,则可采用引用计数方式:1. 读的时候用CAS把计数加1,读,再用CAS减1;2.写的时候用CAS赋值新指针(看整个pair是否变化,即pMap没变化且count为1,则赋值新指针); 巧妙之处是把pMap和count放到同一个pair里,64位,在一个CAS里可以操作;缺点是读太多的话,写操作就饿死了;