0 介绍
对多线程编程有过了解的朋友一定知道锁的概念,它的作用是为了保证临界区的代码在多线程下能够正常工作,也就是说,锁将保证共享资源在任意时刻只能有一个使用者。
过去博主我也只是知道锁是如何使用的,对其内在原理并不了解。今天看看了看清华大学陈渝老师的网课,对锁的实现有了一定认识,特此记录。
1 原子操作
理解锁的实现,首先要理解Cpu的原子操作。现代的cpu都提供一些特殊的原子操作,他们执行的时候将不能被中断,如CAS(compare and swap)、Test-And-Set。这两种操作均可以实现锁,下面讲讲Test-And-Set是如何实现的。
这个操作并不复杂,对于target,如果它是false,将其修改为true,但返回的值为rv;如果是true,什么也没有发生。这个简单的操作是原子的,下面看看如何由它来实现锁。
boolbean TestAndSet(boolbead *target)
{
boolbead *rv = *target;
*target = true;
return *rv;
}
3 自旋锁
所谓自旋锁,百科定义如下:
自选锁是为实现保护共享资源而提出一种锁机制。自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
通俗来说,如果当前锁被占用,调用者将阻塞在原地,这是与互斥锁不同的地方。对于锁来说,我们一般考虑它的上锁(lock)与解锁(unlock)操作。如下面的c语言伪代码所示,当需要在临界区前上锁时,调用锁的Acquire方法即可。如果当前已经上锁了,value为1,则后来的调用者将阻塞在while处;如果未上锁,将不会阻塞在while,同时test-and-set会将value置为1,防止未解锁前后来的调用者执行临界区代码。解锁只需要将value置为0即可。
class Lock
{
int value = 0;
void Acquire()
{
while(test-and-set(&value))
;
}
void Release()
{
value = 0;
}
};
自旋锁也有一些缺点,首先它是一个忙等待锁,在while处阻塞的时候会消耗cpu资源直到阻塞结束。第二个,自旋锁的使用可能会造成死锁。比如一个低优先级的进程在占用临界区资源时,来了一个高优先级进程夺走了cpu,然后高优先级进程也在acquire锁,但是锁却在低优先级进程手里,这时死锁就发生了。
4 互斥锁
互斥锁与自旋锁不同的地方在于,当调用者发现锁已经被使用的时候,会使当前进程睡眠,加入到等待队列,然后使用调度机制(schedule函数)执行其他进程,而不是原地等待。解锁时拿出队头睡眠的进程唤醒即可。其余和自旋锁一致。
class Lock
{
int value = 0;
WaitQueue q;
void Acquire()
{
while(test-and-set(&value))
{
add this TCB to the waitqueue q;
schedule();
}
}
void Release()
{
value = 0;
remove thread t from q;
wake(t);
}
};