Go 互斥锁的实现原理?

参考解析

Go sync包提供了两种锁类型:互斥锁sync.Mutex 和 读写互斥锁sync.RWMutex,都属于悲观锁。

概念:

Mutex是互斥锁,当一个 goroutine 获得了锁后,其他 goroutine 不能获取锁(只能存在一个写者或读者,不能同时读和写)

使用场景:

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

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

536.Go 互斥锁的实现原理? - 图1

底层实现结构:

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

  1. type Mutex struct {
  2. state int32
  3. sema uint32
  4. }

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

mutex_state

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

mutex_sema

  1. addr = &sema
  2. func semroot(addr *uint32) *semaRoot {
  3. return &semtable[(uintptr(unsafe.Pointer(addr))>>3)%semTabSize].root
  4. }
  5. root := semroot(addr)
  6. root.queue(addr, s, lifo)
  7. root.dequeue(addr)
  8. var semtable [251]struct {
  9. root semaRoot
  10. ...
  11. }
  12. type semaRoot struct {
  13. lock mutex
  14. treap *sudog // root of balanced tree of unique waiters.
  15. nwait uint32 // Number of waiters. Read w/o the lock.
  16. }
  17. type sudog struct {
  18. g *g
  19. next *sudog
  20. prev *sudog
  21. elem unsafe.Pointer // 指向sema变量
  22. waitlink *sudog // g.waiting list or semaRoot
  23. waittail *sudog // semaRoot
  24. ...
  25. }

操作:

锁的实现一般会依赖于原子操作、信号量,通过atomic 包中的一些原子操作来实现锁的锁定,通过信号量来实现线程的阻塞与唤醒

加锁

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

536.Go 互斥锁的实现原理? - 图4

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

解锁

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

mutex_unlock

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

注意点:

- 在 Lock() 之前使用 Unlock() 会导致 panic 异常
- 使用 Lock() 加锁后,再次 Lock() 会导致死锁(不支持重入),需Unlock()解锁后才能再加锁
- 锁定状态与 goroutine 没有关联,一个 goroutine 可以 Lock,另一个 goroutine 可以 Unlock