基于队列的锁:mcs lock简介

今天在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

本文仅用于学习和交流目的,不表明图灵社区观点。非商业转载请注明做译者、出处,并保留本文的原始连接。

相关文章
相关标签/搜索