zoukankan      html  css  js  c++  java
  • 并行编程——锁

    锁的概念是承接原子操作的,根本目的都是为了并发保护,只不过抽象的层次更高,多核环境下的锁的实现要依赖于第三章的理论基础。

    下面是几种常见的锁机制的简介,这里不谈具体实现,具体实现可以参考linux内核代码

    1.1  信号量

    Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。

    信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况;只能在进程上下文中使用,因为中断上下文中是不能被调度的;另外当代码持有信号量时,不可以再持有自旋锁。

     1.2  自旋锁

    自旋锁最多只能被一个可执行线程持有,如果一个执行线程试图请求一个已被争用(已经被持有)的自旋锁,那么这个线程就会一直进行忙循环——旋转——等待锁重新可用。要是锁未被争用,请求它的执行线程便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的执行线程同时进入临界区。

    事实上,自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。另外自旋锁不允许任务睡眠(持有自旋锁的任务睡眠会造成自死锁),它能够在中断上下文中使用。

     1.3  读写锁

    读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

     在读写锁保持期间也是抢占失效的。

    如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。

    下面是X86上一种读写锁的实现:

    图1 这把锁的核心是一个 volatile int32 的变量,这个变量值为-1的时候表示处于写锁状态,值大于0表示处于读锁状态。

    图2 通过原子CAS操作实现读锁获取,多个线程可以同时调用这个函数获取读锁,同时也可能有其他线程在申请写锁,如果这时锁处于写锁持有状态,x<0 ,则所有申请读锁的线程会循环等待,可见这是一种特殊的自旋锁。否则说明锁处于读锁状态或0状态,可以进入CAS操作尝试申请,每个想持有的线程原子地为变量cnt 加1,成功了就相当于获取了读锁,否则说明其他线程获取了,这里的其他线程既有可能是其他申请读锁的线程,也有可能是申请写锁的线程(x一开始为0时),所以CAS失败后需要重新判断x是否小于0,然后再调用CAS申请

    图3 释放读锁的操作没有使用原子原语,而是通过总线锁和指令 decl 实现为cnt减1操作,也可以调用 atomic_dec 为 cnt 做原子减1操作

    图4 写锁的申请跟读锁申请类似,如果x不为0,说明还有线程持有读锁,则循环等待,等全部持有的读锁都释放了,x变回0,这时通过CAS操作为x赋值-1,成功则申请了一把写锁。

    图5 写锁的释放跟读锁释放相反,写锁释放是atomic_inc的操作,inc加1后又变回0

    1.4  顺序锁

    顺序锁也是对读写锁的一种优化,对于顺序锁,读者绝不会被写者阻塞,也就说,读者可以在写者对被顺序锁保护的共享资源进行写操作时仍然可以继续读,而不必等待写者完成写操作,写者也不需要等待所有读者完成读操作才去进行写操作。但是,写者与写者之间仍然是互斥的,即如果有写者在进行写操作,其他写者必须自旋在那里,直到写者释放了顺序锁。

     这种锁有一个限制,它必须要求被保护的共享资源不含有指针,因为写者可能使得指针失效,但读者如果正要访问该指针,将导致OOPs。

     如果读者在读操作期间,写者已经发生了写操作,那么,读者必须重新读取数据,以便确保得到的数据是完整的。

     这种锁对于读写同时进行的概率比较小的情况,性能是非常好的,而且它允许读写同时进行,因而更大地提高了并发性。

    1.5  RCU

    RCU(Read-Copy Update),顾名思义就是读-拷贝修改,它是基于其原理命名的。对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。这个时机就是所有引用该数据的CPU都退出对共享数据的操作。

     RCU也是读写锁的高性能版本,但是它比大读者锁具有更好的扩展性和性能。 RCU既允许多个读者同时访问被保护的数据,又允许多个读者和多个写者同时访问被保护的数据(注意:是否可以有多个写者并行访问取决于写者之间使用的同步机制),读者没有任何同步开销,而写者的同步开销则取决于使用的写者间同步机制。但RCU不能替代读写锁,因为如果写比较多时,对读者的性能提高不能弥补写者导致的损失。

  • 相关阅读:
    【BZOJ4892】【TJOI2017】—DNA(后缀数组+ST表)
    【BZOJ1563】【NOI2009】—诗人小G(决策二分栈优化dp)
    【洛谷P5249】【LnOI2019】—加特林轮盘赌(概率dp)
    【Ural1519】— Formula1(轮廓线dp)
    【BZOJ3728】【PA2014】—Final Zarowki(思维题)
    【BZOJ3730】—震波(动态点分治)
    【Hackerrank (70)】—Range Modular Query(莫队+暴力)
    【省选模拟】—Cactus(圆方树+dfs序)
    【BZOJ2125】—最短路(圆方树+树链剖分)
    python 基础 列表
  • 原文地址:https://www.cnblogs.com/jiayy/p/3246229.html
Copyright © 2011-2022 走看看