大家好,我是木川

一、什么是互斥锁

Go语言中的互斥锁(Mutex)是一种关键的并发控制机制,用于保护共享资源免受多个Goroutine的并发访问。

互斥锁的主要目标是确保一次只有一个Goroutine可以访问被锁定的共享资源。在Go语言中,互斥锁由sync包提供,并且具有sync.Mutex类型。互斥锁的基本操作包括加锁(Lock)和解锁(Unlock)。

var mu sync.Mutex

func main() {
    mu.Lock()
    // 访问共享资源
    mu.Unlock()
}

使用场景:多个线程同时访问临界区,为保证数据的安全,锁住一些共享资源, 以防止并发访问这些共享数据时可能导致的数据不一致问题。

获取锁的线程可以正常访问临界区,未获取到锁的线程等待锁释放后可以尝试获取锁



​面试官:谈谈 Go 互斥锁实现原理_golang

二、互斥锁的实现原理

Go语言的互斥锁的实现原理可以简单概括为:

  1. 互斥锁的零值是未加锁状态,即初始状态下没有任何Goroutine拥有锁。
  2. 当一个Goroutine尝试获取锁时,如果锁处于未加锁状态,它会立即获得锁,将锁状态置为已加锁,并继续执行。
  3. 如果锁已经被其他Goroutine持有,那么当前Goroutine将被阻塞,直到锁被释放。
  4. 当一个Goroutine释放锁时,锁的状态将被设置为未加锁,此时等待的Goroutine中的一个将被唤醒并获得锁。

底层实现结构

互斥锁对应的是底层结构是sync.Mutex结构体,,位于 src/sync/mutex.go中

type Mutex struct {  
  state int32  
  sema  uint32
 }

state表示锁的状态,有锁定、被唤醒、饥饿模式等,并且是用state的二进制位来标识的,不同模式下会有不同的处理方式



​面试官:谈谈 Go 互斥锁实现原理_golang_02

sema表示信号量,mutex阻塞队列的定位是通过这个变量来实现的,从而实现goroutine的阻塞和唤醒



​面试官:谈谈 Go 互斥锁实现原理_java_03

addr = &sema
func semroot(addr *uint32) *semaRoot {  
   return &semtable[(uintptr(unsafe.Pointer(addr))>>3)%semTabSize].root  
}
root := semroot(addr)
root.queue(addr, s, lifo)
root.dequeue(addr)

var semtable [251]struct {  
   root semaRoot  
   ...
}

type semaRoot struct {  
  lock  mutex  
  treap *sudog // root of balanced tree of unique waiters.  
  nwait uint32 // Number of waiters. Read w/o the lock.  
}

type sudog struct {
 g *g  
 next *sudog  
 prev *sudog
 elem unsafe.Pointer // 指向sema变量
 waitlink *sudog // g.waiting list or semaRoot  
 waittail *sudog // semaRoot
 ...
}

加锁

通过原子操作cas加锁,如果加锁不成功,根据不同的场景选择自旋重试加锁或者阻塞等待被唤醒后加锁



​面试官:谈谈 Go 互斥锁实现原理_java_04

func (m *Mutex) Lock() {
 // Fast path: 幸运之路,一下就获取到了锁
 if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
  return
 }
 // Slow path:缓慢之路,尝试自旋或阻塞获取锁
 m.lockSlow()
}

解锁

通过原子操作add解锁,如果仍有goroutine在等待,唤醒等待的goroutine



​面试官:谈谈 Go 互斥锁实现原理_linux_05

func (m *Mutex) Unlock() {  
   // Fast path: 幸运之路,解锁
   new := atomic.AddInt32(&m.state, -mutexLocked)  
   if new != 0 {  
    // Slow path:如果有等待的goroutine,唤醒等待的goroutine
   m.unlockSlow()
   }  
}

这种实现原理保证了只有一个Goroutine能够同时访问临界区,从而确保了并发访问的安全性。

三、互斥锁的注意事项

  • 在 Lock() 之前使用 Unlock() 会导致 panic 异常
  • 使用 Lock() 加锁后,再次 Lock() 会导致死锁(不支持重入),需Unlock()解锁后才能再加锁
  • 锁定状态与 goroutine 没有关联,一个 goroutine 可以 Lock,另一个 goroutine 可以 Unlock
  • 在高度竞争的情况下,多个Goroutine争夺锁可能导致性能下降。为了提高性能,可以考虑使用更轻量级的同步原语,如读写锁(sync.RWMutex)或通道(chan),以根据需求进行读或写的并发控制。
  • 互斥锁不适合用于允许多个线程同时读取共享资源的情况。如果您的应用程序需要支持多个线程并发读取但在写入时仍然需要互斥访问,可以考虑使用读写锁