今天在Quora闲逛,看到一个对于MCS Lock的问答。答题的哥们深刻浅出的写了一大篇,感受很是不错,特意翻译出来。html
原文翻译node
要理解MCS Locks的本质,必须先知道其产生背景(Why),而后才是其运做原理。就像原论文提到的,咱们先从spin-lock提及。spin-lock 是一种基于test-and-set操做的锁机制。算法
<!-- lang: cpp --> function Lock(lock){ while(test_and_set(lock)==1); } function Unlock(lock){ lock = 0; }
test_and_set是一个原子操做,读取lock,查看lock值,若是是0,设置其为1,返回0。若是是lock值为1, 直接返回1。这里lock的值0和1分别表示无锁和有锁。因为test_and_set的原子性,不会同时有两个进程/线程同时进入该方法, 整个方法无须担忧并发操做致使的数据不一致。缓存
一切看来都很完美,可是,有两个问题:(1) test_and_set操做必须有硬件配合完成,必须在各个硬件(内存,二级缓存,寄存器)等等之间保持 数据一致性,通信开销很大。(2) 他不保证公平性,也就是不会保证等待进程/线程按照FIFO的顺序得到锁,可能有比较倒霉的进程/线程等待很长时间 才能得到锁。架构
为了解决上面的问题,出现一种Ticket Lock的算法,比较相似于Lamport's backery algorithm。就像在面包店里排队买面包同样,每一个人先付钱,拿 一张票,等待他手中的那张票被叫到。下面是伪代码并发
<!-- lang: cpp --> ticket_lock { int now_serving; int next_ticket; }; function Lock(ticket_lock lock){ //get your ticket atomically int my_ticket = fetch_and_increment(lock.next_ticket); while(my_ticket != now_serving){}; } function Unlock(ticket_lock lock){ lock.now_serving++; }
这里用到了一个原子操做fetch_and_increment(实际上lock.now_serving++也必须保证是原子),这样保证两个进程/线程没法获得同一个ticket。 那么上面的算法解决的是什么问题呢?只调用一次原子操做!!!最原始的那个算法但是不停的在调用。这样系统在保持一致性上的消耗就小不少。 第二,能够按照先来先得(FIFO)的规则来得到锁。没有插队,一切都很公平。学习
可是,这还不够好。想一想多处理器的架构,每一个进程/线程占用的处理器都在读同一个变量,也就是now_serving。为何这样很差呢,从多个CPU缓存的 一致性考虑,每个处理器都在不停的读取now_serving自己就有很多消耗。最后单个进程/线程处理器对now_serving++的操做不但要刷新到本地缓存中,并且 要与其余的CPU缓存保持一致。为了达到这个目的,系统会对全部的缓存进行一致性处理,读取新值只能串行读取,而后再作操做,整个读取时间是与处理器个数 线性相关。fetch
说到这里,才会聊到mcs队列锁。使用mcs锁的目的是,让得到锁的时间从O(n)变为O(1)。每一个处理器都会有一个本地变量不用与其余处理器同步。伪代码以下atom
<!-- lang: cpp --> mcs_node{ mcs_node next; int is_locked; } mcs_lock{ mcs_node queue; } function Lock(mcs_lock,mcs_node my_node){ my_node.next = NULL; mcs_node predecessor = fetch_and_store(lock.queue,my_node); if(predecessor!=NULL){ my_node.is_locked = true; predecessor.next = my_node; while(my_node.is_locked){}; } } function Unlock(mcs_lock lock,mcs_node my_node){ if(my_node.next == NULL){ if(compare_and_swap(lock.queue,my_node,NULL){ return ; } else{ while(my_node.next == NULL){}; } } my_node.next.is_locked = false; }
此次代码多了很多。可是只要记住每个处理器在队列里都表明一个node,就不难理解整个逻辑。当咱们试图得到锁时,先将本身加入队列,而后看看有没有其余 人(predecessor)在等待,若是有则等待本身的is_lock节点被修改为false。当咱们解锁时,若是有一个后继的处理器在等待,则设置其is_locked=false,唤醒他。线程
在Lock方法里,用fetch_and_store来将本地node加入队列,该操做是个原子操做。若是发现前面有人在等待,则将本节点加入等待节点的next域中,等待前面的处理器唤醒本节点。 若是前面没有人,那么直接得到该锁。
在Unlock方法中,首先查看是否有人排在本身后面。这里要注意,即便暂时发现后面没有人,也必须用原子操做compare_and_swap确认本身是最后的一个节点。若是不能确认 必须等待以后节点排(my_node.next == NULL)上来。最后设置my_node.next.is_locked = false唤醒等待者。
最后咱们看一下前面的问题是否解决了。原子操做的次数已经减小到最少,大多数时候只须要本地读写my_node变量。
注释
1.原文来自于 http://www.quora.com/How-does-an-MCS-lock-work.
2.论文来自于 http://www.cs.rochester.edu/u/scott/papers/1991_TOCS_synch.pdf.
3.一些伪代码 http://www.cs.rochester.edu/research/synchronization/pseudocode/ss.html
4.关于多处理器的架构下的共享变量访问的机制能够看看Java memory model或者搜一下NUMA与SMP架构,这方面本人也不是特别懂,还请各位赐教。
4.熟悉Java concurrent包的同窗能够本身实现一下上面几个算法,可用的类和方法有:
AtomicReferenceFieldUpdater.compareAndSet().对应compare_and_swap AtomicReferenceFieldUpdater.getAndSet().对应fetch_and_store AtomicInteger.getAndIncrement().对应fetch_and_increment
本文仅用于学习和交流目的,不表明图灵社区观点。非商业转载请注明做译者、出处,并保留本文的原始连接。