Lock
mutex, spinlock, semaphore, RCU一次看一下
mutex
mutex有一票比较好的文章,我这里搬运一下。
Mutex: 如果一个线程试图获取一个 mutex,但是没有成功,因为 mutex 已经被占用, 它将进入睡眠,让其他进程运行,直到 mutex 被其他进程释放.
这就意味着Mutex将使得线程睡眠,然后再通过notify唤醒它们,两者都是开销比较大的操作,也就是 context switch 的开销。关于消耗,可以看之前的cpu cycle的文章
在 Lock 和 Unlock 之间的代码,一般被称为 critical section.
Mutex 也包含一些复杂的类型,如下:
- Recursive: 允许占有锁的那一个线程再次获取同样的锁,对递归算法是必要的.
- Queuing: 使得 公平 的获取锁,通过 FIFO 排序锁的请求.
- Reader/Writer(rwlock): 允许多个 reader 同时获取锁,如果有 reader 占用锁,writer 只有等到 reader 释放锁.
- Scoped: RAII 类型定义的锁获取和解锁.
但 Mutex 也会引入其他一些问题,如deadlock和priority inversion.
Condition Variables
Mutex 变量如锁一般防止多个线程访问共享数据资源,如果某个线程等待某个共享数据达到某个数值才进行相应的操作,那么这个线程需要不断的去 poll,查看是否满足需要的值,这样开销很大,因为线程需要一直处于忙状态.
引入 Condition Variables 来完成这样的同步到某个实际数据值而不要不断 poll.
比如,程序有一个计数器,当计数器达到某一个值时去激活某个线程运行.把计数器当成一个 Condition variable.这个线程可以等待这个 Condition variable,其他 active 线程操作完这个 Condition variable,可以通过 signal/broadcast 去唤醒那些等待这个 Condition variable 睡眠的线程.
Condition 变量一般与 mutex 一起使用.锁住查看的共享数据资源.
缺点也比较明显,看ref里的文章,lock的lock和release其实不慢,但是Lock Contention恶心
Semaphore
当某些资源具有多个时,简单的 Mutex 不能满足,引入 Semphore,Semphore 可以根据资源个数初始化为任意值.当线程们占有所有资源,使得 Semphore 为 0,那么其他线程再获取资源只有等待.当 Semphore 值只能是 1 或 0 时,它相当于简单的 Mutex.
R/W lock
共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作,读操作可并发重入,写操作是互斥的。
如果当前没有writer,那么多个reader可以同时获取这个rwlock。如果当前没有任何的reader,那么一个writer可以获取这个rwlock。
但有一种饥饿的情况,读者巨几把多,但是这个时候突然有一个要写的,那么这个写者就会一直等待,直到所有的读者都释放了锁。所以读写锁的使用通过是要考虑场景的
seq lock
之前单写,或者多读,这次可以支持读写并发
写,在写者开始更新数据时,它会增加 seqlock 的序列号。当写者完成更新后,再次增加序列号。因此,如果序列号是奇数,表示有写者正在更新数据;如果是偶数,表示没有写者正在更新数据。
读,在读者开始读取数据时,它会读取当前的序列号。然后,读者读取数据,并再次读取序列号。如果两次读取的序列号不同,或者序列号是奇数,表示有写者正在更新数据,那么读者需要重新读取数据。否则,表示读者读取的数据是有效的。
在写操作比较少的时候,或者写并发不高,看起来seq lock是优于读写锁的
缺点也比较明显
同时写多个:如果有多个写者并发更新数据,它们可能会互相干扰,导致读者需要多次重新读取数据。
写的很勤快,读者可能会长时间得不到有效的数据
RCU
可以认为kernel里关于RCU的代码全部是Paul E. McKenney写的,所以有必要读一下他的perfbook。
RCU看起来是在拿空间换多读多写,分4个阶段考虑
- 读取
读取者通过 RCU 保护的数据结构进行遍历时,不需要获取锁,也不需要进行任何特殊的处理。这意味着多个读取者可以并发地读取数据结构。
- 更新
首先,更新者不能直接更改原始数据结构,而是需要创建一个新的副本,并在副本上进行更改。然后,更新者将数据结构的指针从旧版本切换到新版本。这个切换操作是原子的,也就是说,读取者要么看到旧版本,要么看到新版本,但不会看到中间状态。
- wait->grace preiod
在切换版本后,更新者不能立即删除旧版本,因为可能仍有读取者正在使用它。因此,更新者需要等待所有可能的读取者完成他们的操作。这个等待阶段被称为 “grace period”。
- 删除
一旦所有的读取者都完成了他们的操作,更新者就可以安全地删除旧版本了。
有点比较多,缺点我感觉也是有
RCU对内存使用看起来比较大,这是缺点之一,
第二个就是写操作其实比较复杂,会不会带来一定的写延迟(创建副本,删除旧版本)
他的更换操作是原子的,这就意味着比较依赖硬件特性了,硬件特性一般都比较恶心
随便看看->