How to implement Golang Mutex golang 是如何实现互斥锁的
在开始之前,我们需要知道锁实现的几种方式。
# 信号量 操作系统中有 P 和 V 操作。P 操作是将信号量值减去 1,V 操作是将信号量值增加 1.因此信号量的操作模式为:
初始化,给它一个非负整数值。
在程序试图进入临界区之前需要先运行 P 操作,然后会有两种情况。
当信号量 S 减少到负值时,该过程将被阻止并且无法继续。这个时候,进程会被阻塞。
当信号量 S 不为负时,进程可以进入临界区。
在程序离开临界区时,需要执行 V 操作。当信号量 S 不是为负时,之前被阻止的其他进程将允许进临界区。
# 信号量和锁 尽管信号量和锁看起来相似,例如,当信号量为 1 时,实现了互斥锁,但实际上,它们具有不同的含义。
锁用于保护临界资源,例如读和写不能同时执行场景。
信号量是用于确保进程 (线程或 goroutine) 被调度。比如,三个进程共同计算 c = a+b
。首先,a+b
的计算和赋值操作不能同时执行。其次,必须确保首先执行 a+b
。c 在赋值之后执行,因此这个位置需要以信号量的形式执行。
此外,可以通过信号量实现锁,然后 goroutine 可以根据规则阻塞和唤醒锁;也可以通过自旋的方式实现锁,goroutine 将持有 CPU,直到解锁。
这两种方式之间的区别是是否需要调度 goroutine,但是从本质上讲,锁是为了确保不会错误地访问临界资源。
# 自旋锁 CAS 理论是一种自旋锁。
在同一时间只有一个线程获得锁,而没有获得锁的线程通常有两种处理方式:
一直循环等待,以确定资源是否释放了锁。这种锁称为自旋锁,它不会阻塞线程(NON-BLOCKING)。
一直阻塞,等待重新调度,这种是互斥锁。
自旋锁的原理相对简单。如果持有锁的线程可以在短时间内释放锁定资源,那么等待锁的其他线程不需要在内核态和用户态之间切换来回切换阻止状态,他们只需要通过自旋的方式等一会,等待持有锁的线程释放锁,然后获取锁,这种方式避免用户进程在内核切换。
但如果长时间未释放锁,那么自旋锁的开销会非常大,它会阻止其他线程的运行和调度。
线程持有锁的时间越长,持有锁的线程被 OS 调度中断的风险就越大。
如果发生中断,他线程将保持自旋状态(反复尝试获取锁),而持有锁的线程不打算释放锁,这将导致无限延迟,直到持有锁的线程完成并释放锁。
解决上述情况的一个好方法是为自旋锁定设置一个自旋时间,并在时间一到就释放自旋锁。
# #悲观锁定和乐观锁定 悲观锁定是一种悲观思维。它总是认为最坏的情况可能会发生。它认为这些数据很可能被其他人修改过。无论是读还是写,悲观锁都是在执行操作之前锁定的。
乐观锁定的思想与悲观锁定的思想相反。它始终认为资源和数据不会被别人修改,所以读取不会被锁定,但是乐观锁定会在写操作时确定当前数据是否被修改过。
乐观锁定的实现方案主要包括 CAS 和版本号机制。乐观锁定适用于多读场景这可以提高吞吐量。
CAS 是一个著名的基于比较和交换无锁算法。
也就是在不使用锁的情况下实现多个线程之间的变量同步,(即在不阻塞线程的情况下实现变量同步),所以又称非阻塞同步。
CAS 涉及三种关系: 指向内存区域的指针 V、旧值 a 和要写入的新值 B。
由 CAS 实现的乐观锁将带来 ABA 问题。同时,整个乐观锁会在数据不一致的情况下触发等待和重试机制,这对性能有很大的影响。
版本号机制通过版本的值实现版本控制。
有了以上的基础知识,我们就可以开始分析 golang 是如何实现互斥锁的了。
Golang 的 Mutex 实现一直在改进,到目前为止,主要经历了 4 个版本:
V1: 简单实现的版本
V2: 新的 goroutine 参加锁的竞争
V3: 新的 goroutines 更多参与竞争的机会
V4: 解决老 goroutine 饥饿的问题
每一次改进都是为了提高系统的整体性能。这个升级是渐进的、持续的,因此有必要从 V1 版本开始慢慢地看 Mutex 的演变过程。
V1: 简单实现的版本 在 V1 版本中,互斥的完整源代码如下。commit[1]
核心代码如下:
func cas(val *int32, old, new int32) bool
func semacquire(*int32)
func semrelease(*int32)
// The structure of the mutex, containing two fields
type Mutex struct {
key int32 // Indication of whether the lock is held
sema int32 // Semaphore dedicated to block/wake up goroutine
}
// Guaranteed to successfully increment the value of delta on val
func xadd(val *int32, delta int32) (new int32) {
for {
v := *val
if cas(val, v, v+delta) {
return v + delta
}
}
panic("unreached")
}
// request lock
func (m *Mutex) Lock() {
if xadd(&m.key, 1) == 1 { // Add 1 to the ID, if it is equal to 1, the lock is successfully acquired
return
}
semacquire(&m.sema) // Otherwise block waiting
}
func (m *Mutex) Unlock() {
if xadd(&m.key, -1) == 0 { // Subtract 1 from the flag, if equal to 0, there are no other waiters
return
}
semrelease(&m.sema) // Wake up other blocked goroutines
}
code here[2]
首先,互斥锁的结构非常简单,包括两个字段,key
和 sema
。
key
表示有几个 gorutines 正在使用或准备使用该锁。如果 key==0