libevent c++高并发网络编程_C|并发编程|互斥锁实现

OS:Three Easy Pieces
Lock​pages.cs.wisc.edu

导语

锁Lock,正如现实中的锁一样,决定了对于资源的访问权。在并发编程中,由于资源共享的缘故,一个线程中的write操作有可能影响到另一个线程的read操作。

部分严格的程序员为了杜绝这种side effect,选择了Functional Programming,以确保完全的Thread Safety。而在正常的结构化编程中,程序员倾向于使用锁,防止意料之外的Side Effect。

锁控制了一个资源只能被一个线程同时访问,因此有效避免了多线程情况下的读写导致的异常输出。


自旋Spinning锁

typedef 

这是一个最基本的版本,当flag置1时,锁被获得,而flag置0时,锁被释放。而当锁没有被释放时,程序将会不断检测flag,而不做任何实际事情,因此被称作自旋。

原子性Atomicity

这个版本存在着一个致命的bug,因为CPU调度并不保证Line a与Line b之间不会插入其他线程的代码。如果在line a之后其他线程已经获得了锁,那么line ba仍然会被执行,也就是说flag的检测和设置被分开了,导致同时有两个线程持有这把锁。

我们一般使用atomic exchange来保证获取锁会是原子性操作,要么同时完成flag的检测和设置,要么什么都不做。这里C代码形式如下(TestAndSet):

int 

而另一种形式为(CompareAndSwap),作用类似,但比TestAndSet更为泛用

int 

饥饿Starvation

由于CPU调度并不保证先试图获取锁的必定能先获得,可能出现某个线程很久无法获得锁的情况。一个简单的想法就是使用队列,保证FIFO。

我们先以FetchAndAdd作为原子性的后置++操作。

int 

正如同餐厅叫号,turn表示当前的号码,ticket表示手中的号码,每一个顾客(线程)用完之后就呼叫下一个号码。

Sleeping锁

由于自旋锁导致每个线程都在执行while操作,空转造成了极大浪费,因此一种改进思路是:在没有获得锁之前,令线程直接沉睡。而当释放锁时,再唤醒下一个线程。同理,我们使用queue作为数据结构,但是维护一个显式的链表。

typedef 

lock_t:

这里的flag表示锁有没有被线程需求,锁可以同时被多个线程所等候,仅当没有线程等候时才会置0。

而guard是lock和unlock过程的一个自旋锁。在过程结束后自动释放。(basically as a spin-lock around the flag and queue manipulations the lock is using)

void 

lock:

当锁中队列为空时: 置flag为1,即flag锁被占用

当锁中队列不为空时: 入队,使用park操作令线程休眠等待唤醒。

void 

unlock:

当锁中队列为空时:置flag为0,即此锁闲置

当锁中队列不为空时:出队,使用unpark操作唤醒下一个线程并释放锁

Buggy

queue_add

假如在line a和line b之间正好有一个线程unlock了,那么将会唤醒当前正在加锁的线程,然后再运行line b使得当前线程进入休眠,而队列中当前线程却已经出队。这样一来,陷入休眠的当前线程就不再可以被唤醒了。

为了解决这个问题,如果能直接让ab原子性就好了,然而实际情况却很难做到。

我们可以特异性针对上面的问题处理,例如某种实现中,setpark函数可以令程序进入准备park的状态,如果在park之前进程已经被unpark,那么park将直接返回

queue_add

Two-phase锁

实际操作系统中,互斥锁的实现综合了以上两种锁的实现。以下是Linux的Mutex实现机制。

膜这段代码!!!

void 

Two-phase 锁意识到对于那些将会马上被释放的锁,使用自旋锁更有益处。而唤醒等操作需要使用更多的sys-call,因此会增大开销。

futex_wait和futex_wake会在内核态维护一个mutex对应的队列。

在第一阶段,线程将会自旋若干次,试图获取锁。

一旦第一阶段没有完成,则会进入第二阶段,线程沉睡,直到锁被释放后将线程唤醒。

上述linux的实现只自旋了一次,但是也可以使用有固定自旋次数的循环。

注意:

这里setpark的原因和上面不同,因为这里不会出现先入队列再沉睡的情况。

Special case: Queue Empty

假如没有v>=0的判断,

假如B lock中间插入C unlock,由于队列为空,lock位变为0,不wake下一个线程。此时B wait,则无法被唤醒。

但是如果continue,B就能直接拿锁,而不会wait。

联想

我个人其实是把这个和轮询/中断类比的,

以IO为例,轮询需要CPU不断访问IO,而中断则是仅当IO发生改变时CPU才进行访问。

同理,Spin其实就是CPU不断判断锁,而Sleep则是仅当锁被释放时才唤醒下一个线程。

尽管所处的抽象层次不同,但是这种思想确实有共通之处。

再往上,其实观察者模式也类似,查询其他对象是否发生改变很困难,因为在不知情的情况下,每时每刻都有可能发生改变。但改变的当事人想通知查询者就很容易,只需要在改变的同时发出消息即可。

猜想或者归纳一下,或许这种现象是由于信息隔离导致的。正是因为信息交流的双方信息的不对等,才导致传递这种信息所需要的代价截然不同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值