OSTEP学习笔记之-Lock的实现
并发问题中我们需要对一系列的指令进行“原子操作”,但是由于中断和多处理器的存在,这无法保证,因此引入了Lock的概念。我们将critical section用Lock标注包围,从而使之成为原子操作。
比如说要对以下代码进行原子操作
以上代码包含三个基本指令:取值、相加、存储,因而有可能中途被打断,我们可以执行如下的操作。
代码中的mutex
就是Lock
,如果mutex
是available
,那么就会立即返回线程进入执行被保护代码,如果当前是acquired
,那么就会被block
一直等待,直到mutex
状态变为available
。mutex
意为mutual exclusive
,它也包含其它信息,如当前拥有它的线程,以及预定该Lock
的队列等,但对用户并不可见。
|
|
Lock的实现
操作系统要实现Lock
,需要硬件的支持,在设计实现方式时,需要考虑以下几点
1、mutual exclusion 基本功能,必须保证排它性
2、fairness 有相等的机会获取锁,不能有线程starve
3、performance
假设没有硬件支持,直接使用普通变量作为mutex
,那么在测试与获取之间,就有可能发生“错误”操作:线程1和线程2可能交替地测试lock
为可获取并进入critical section
。所以要实现mutex
,必须test and set
是一个原子操作,需要由硬件提供。同时在等待lock
的wait
阶段,如果没有硬件支持,线程必须不停地spin
,会占用大量的CPU
资源,特别在单处理器系统中,会导致持有lock
的线程难以运行,造成效率的极大浪费。
Test and Set
硬件提供基本指令,获取当前值当更新为指定值,一个指令内完成,自然也是原子操作。用伪代码示意如下:
使用方法如下
这种方法可以保证mutex
,但是却依然有spin
消耗CPU
的问题。
compare-and-exchange
整体来说与test and set
有些类似,不过使用了指针。伪代码表示如下:
如果ptr
指向值与expected
相等,则更新为指向值为new
,否则不操作,返回值为*ptr
。
使用方法也与test and set
相同
Fetch-And-Add
简单地说,就是将i++
做成一个基本指令。
|
|
使用方式
这种方式使用了两个变量组成一个结构体来构成锁,其中一个代表自己的ticket
,另一个代表轮盘。lock
操作即取票并与轮盘比对,符合即进入执行,否则spin
。unlock
则将轮盘前拨1位(+1)。
这种方式,较之前方案有一个明显特点,就是可以防止Stare
。每个线程都会轮番地得到资源。
sleep代替spin
上述的方案,都在解决mutex
问题(互斥),但是没有改善spin
消费CPU
的问题。思路很简单,也是由硬件和OS
提供支持,在线程获取lock
失败后,让其进行休眠,并加入OS
管理的队列,在lock
被释放后,由OS
来依次唤醒队列中休眠的线程。
|
|
上述方式为了能让等待线程能尽快进行sleep
(park)
,使用了一个guard
来保证mutex
,同时用flag
来代表真正的lock
。获得gurad
之后,可以实现原子操作,但不代表可以执行critical section
。如果flag
(lock
)可用,将flag
置位,进入执行。如果不可用,则将线程加入队列并休眠。两种情况下都会在获得gurad
独占后将gurad
及时复位,因而理论上不会造成其它线程的长时间spin
。
需要注意的是unlock
的过程,当有线程在队列中时,unlock
并没有释放falg(lock)
,因为unpark
之后,被唤醒的线程会直接进入执行(从lock
返回),因而无需也不能释放lock
。只有在无线程处于等待时,才释放lock
。
但这也引入了一个race condition
的问题。假设只有两个线程,其中一线程在park
前被context switch
到拥有lock
的线程,然后lock
被释放,那么获取lock
的线程将立即进入park
,从而永久沉睡。此称为wakeup/waiting race
.
一个解决方案是增加一个system call
:setpark()
。表示将要执行park
,但如果期间被中断并有其它线程调用了unpark
,那么setpart
会立即返回,并不进行park
.
另一个解决方案是将guard
置于kernel
中,当上述情况发生时kernel
可以释放lock
并dequeue
运行中的线程。
Two-Phase Locks
Linux
使用了较老风格的tow-phase lock
, 在第一个phase
,它会spin
等待lock
,如果获取失败,进入Phase 2
,则开始sleep
,直到Lock
可用。
总之lock
的实现需要硬件+OS
的支持,而设计一个通用性强的lock
则是富有挑战的。