Golang Mutex互斥锁源码分析
介绍
Golang的Mutex互斥锁机制是一种非常重要的并发控制方式,它可以保证在同一时刻,同一共享资源只能被一个goroutine访问,其他的goroutine必须等待当前访问者释放锁之后才能访问该共享资源。
在使用Mutex机制时,需要进行锁定、解锁等操作,而这一过程是由Mutex的底层实现——sync包来完成的。
源码分析
Mutex实现了Locker接口,其底层包括一个int32类型的state字段(代表锁的状态)、一个chan类型的sema字段(用于goroutine的同步)及相关的操作方法Lock、Unlock等。下面我们就来详细分析Mutex的源码。
1. 变量定义
首先看一下Mutex的定义:
type Mutex struct {
state int32
sema uint32
}
- state是int32类型的字段,表示锁的状态,0表示未加锁状态,1表示已加锁状态。
- sema是uint32类型的字段,用于goroutine的同步操作。当一个goroutine对该锁加锁时,sema会被减1,当goroutine解锁时,sema会被加1;当sema的值为0时,新的goroutine就必须等待已持有锁的goroutine释放锁后才能再次尝试加锁。
2. 方法实现
Lock
在加锁时,Mutex会首先检测是否已经被锁定,如果未被锁定,则获取锁,返回;如果已被锁定,则goroutine会被阻塞并放入wait list中,等待锁的释放。Mutex的Lock方法实现如下:
func (m *Mutex) Lock() {
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { // 如果当前为未锁定状态,则设置为已锁定状态
return
}
m.lockSlow() // 如果已被锁定,放入wait list中等待锁的释放
}
其中,mutexLocked是一个常量,表示Mutex已被锁定,其值为1(未锁定状态的值为0)。CompareAndSwapInt32是一个原子操作,用于比较并交换m.state的值,如果当前值为0,则将其设置为mutexLocked(1),此操作是原子的。原子操作可以保证同时只有一个goroutine可以执行。
如果当前状态已被设置为1,则执行lockSlow方法。
锁竞争
lockSlow是一个私有方法,它实现了Mutex的锁竞争控制,即竞争锁的goroutine会被阻塞并放入wait list中,等待锁的释放。其中,等待的goroutine状态为G_WAITING,表示它处于等待锁的状态。
func (m *Mutex) lockSlow() {
awoke := false
itr := runtime(0) // 等待的自旋次数
for {
if m.state < 0 || !atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { // 锁已经被持有,将当前goroutine的状态设置为G_WAITING
runtime.Gosched() // 让出CPU时间片,让其他goroutine有机会执行
if !awoke { // 设置为可运行状态
runtime_SemacquireMutex(&m.sema, false, itr+1)
awoke = true
} else { // 等待其他goroutine释放锁
runtime_SemacquireMutex(&m.sema, true, 1)
}
itr = 0 // 重置自旋次数
continue
}
if awoke { // 解锁
runtime_Semrelease(&m.sema, false, 1)
}
break
}
}
在等待锁的过程中,lockSlow在当前goroutine被阻塞之后,会进行一定的自旋等待,以便更快地响应锁的释放;同时,也会让出CPU时间片,给其他的goroutine执行机会。如果锁的状态变更了(可能是其他goroutine解锁了),那么锁等待的goroutine会被唤醒。
然后,程序将等待的goroutine状态设置为G_WAITING,代表该goroutine需要等待锁的释放,并调用runtime_SemacquireMutex方法进入等待状态。
Unlock
在解锁时,Mutex会首先使用CompareAndSwapInt32原子操作将持锁的状态设置为未加锁状态0。如果设置成功,则表示释放锁成功,将locked变量设置为unlock,则之前在wait list中等待的goroutine可以继续执行了。如果失败,说明锁已被其他goroutine持有,将lock状态设置为0,表示该goroutine不再持有锁了。Mutex的Unlock方法实现如下:
func (m *Mutex) Unlock() {
if atomic.CompareAndSwapInt32(&m.state, mutexLocked, 0) {
return
}
m.unlockSlow()
}
其中,如果解锁成功(即当前状态为mutexLocked),则直接返回;否则,调用unlockSlow方法。
解锁
unlockSlow是一个私有方法,实现了Mutex的解锁机制,即执行解锁操作的goroutine需要将锁的状态设置为0。对于处于wait list中等待锁的goroutine,unlockSlow方法会释放它们的等待状态,使它们能够继续执行。代码如下:
func (m *Mutex) unlockSlow() {
for i := 0; ; i++ { // 从wait list中取出等待的goroutine解锁
old := m.state
new := int32(0)
if old == 0 {
panic("sync: unlock of unlocked mutex")
}
if old < 0 {
new = old + mutexLocked // 等待goroutine的状态改为G_RUNNABLE,准备继续执行
}
if atomic.CompareAndSwapInt32(&m.state, old, new) {
if old == mutexLocked {
// unlock a locked mutex, no contention
return
}
if new == 0 {
// we've gone from a wait state to no wait state.
// 从等待状态到非等待状态
runtime_Semrelease(&m.sema, true, 1)
}
return
}
if i < 4 || runtime_canSpin(i) {
runtime.Gosched() // 让出CPU时间片
} else {
time.Sleep(time.Duration(rand.Int63n(1)+1) * time.Millisecond) // 防止饥饿
}
}
}
如果解锁的当前状态已经被其他的goroutine修改,则会重试,直到成功为止。
示例分析
下面两个示例,分别展示Mutex的使用及其在goroutine间的同步机制。
示例一
package main
import (
"fmt"
"sync"
"time"
)
var (
x int
mutex sync.Mutex
)
func main() {
fmt.Println("main start")
go func() {
mutex.Lock()
fmt.Println("goroutine start")
x += 1
time.Sleep(time.Duration(2) * time.Second)
fmt.Println("goroutine end")
mutex.Unlock()
}()
time.Sleep(time.Duration(1) * time.Second)
mutex.Lock()
fmt.Println("main lock")
x += 1
fmt.Println("x:", x)
mutex.Unlock()
fmt.Println("main unlock")
time.Sleep(time.Duration(3) * time.Second)
}
- 该示例中包含一个全局int类型变量x和一个Mutex类型的变量mutex。
- main goroutine先通过mutex锁住共享资源x,并对x进行自增操作,输出x的值。
- 然后,该goroutine在mutex锁住的状态下,调用了一个子goroutine,在其中对x进行自增操作,并输出一些信息。
- 子goroutine在两秒钟后自动释放了mutex的锁,main goroutine在三秒钟后也释放了mutex的锁。
运行结果如下:
main start
main lock
x: 1
main unlock
goroutine start
goroutine end
由于mutex被main goroutine锁定,子goroutine必须等待其释放锁之后才能访问共享资源x。在此示例中,我们可以清晰地看到Mutex的同步机制,确保了同一时刻只有一个goroutine正在使用共享资源x。
示例二
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var mutex sync.Mutex
func main() {
wg.Add(2) // 添加两个goroutine
go func() {
mutex.Lock()
defer mutex.Unlock()
fmt.Println("goroutine 1 acquired lock")
time.Sleep(time.Second)
fmt.Println("goroutine 1 released lock")
wg.Done()
}()
go func() {
mutex.Lock()
defer mutex.Unlock()
fmt.Println("goroutine 2 acquired lock")
time.Sleep(time.Second)
fmt.Println("goroutine 2 released lock")
wg.Done()
}()
wg.Wait()
fmt.Println("main goroutine exit")
}
- 该示例中包含两个goroutine,均对mutex进行锁定和解锁操作,共享mutex保护的变量。
- 每个goroutine会先调用mutex的Lock方法获取互斥锁,并在调用启发式方法后,最终释放锁。
运行结果如下:
goroutine 1 acquired lock
goroutine 2 acquired lock
goroutine 1 released lock
goroutine 2 released lock
main goroutine exit
在该示例中,两个goroutine在几乎同时获取了mutex的锁,在mutex被其中一个goroutine释放之前,它们均处于等待状态。当一个goroutine释放了mutex的锁之后,另一个goroutine也才能获取到该锁,继而执行自己的逻辑。
总结
Mutex互斥锁是Golang中非常重要的并发控制方式,其底层实现是sync包。Mutex通过调用Lock、Unlock方法,实现了锁定和解锁等操作,使得同一时刻只有一个goroutine可以访问共享资源。Mutex的机制通过唤醒wait list中的等待goroutine,保证了goroutine之间的同步问题。通过本篇攻略的介绍,可以更好地理解Mutex的实现机制和运行原理。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Golang Mutex互斥锁源码分析 - Python技术站