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

大家好,我是木川

一、什么是互斥锁

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

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

var mu sync.Mutex

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

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

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

173d06d02738bc8bb7506913b830da92.png

二、互斥锁的实现原理

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的二进制位来标识的,不同模式下会有不同的处理方式

f14c50662ff369ac01d6231546c7526a.png

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

2b55678ffecb4b7983aa9c8aca985274.png
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加锁,如果加锁不成功,根据不同的场景选择自旋重试加锁或者阻塞等待被唤醒后加锁

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

解锁

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

8e289b88fe325a9d05deff333b96b373.png
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),以根据需求进行读或写的并发控制。

  • 互斥锁不适合用于允许多个线程同时读取共享资源的情况。如果您的应用程序需要支持多个线程并发读取但在写入时仍然需要互斥访问,可以考虑使用读写锁

最后给自己的原创 Go 面试小册打个广告,如果你从事 Go 相关开发,欢迎扫码购买,目前 10 元买断,加下面的微信发送支付截图额外赠送一份自己录制的 Go 面试题讲解视频

4e1e8d29176288b0f49378815c7d2e95.jpeg

bf5880a0c4d95e0d98cbd7b777ad4f79.png

如果对你有帮助,帮我点一下在看或转发,欢迎关注我的公众号

互斥锁(Mutex)是一种用于多线程编程中保护共享资源的同步机制。它提供了互斥访问的能力,以防止多个线程同时访问共享资源而导致的数据不一致或竞态条件等问题。 在底层实现上,互斥锁通常使用特定的硬件指令或操作系统提供的原子操作来实现。这些原子操作可以确保对共享资源的访问是原子的,即在任何时刻只能有一个线程获得互斥锁的所有权。 一种常见的实现方式是使用原子操作的比较并交换(Compare and Swap,CAS)指令。该指令可以比较内存中的值与预期值是否相等,如果相等则将新值写入内存,并返回操作前的值;如果不相等,则不做任何修改并返回当前值。利用CAS指令,可以实现一个简单的自旋锁。 自旋锁是一种忙等待的锁,当一个线程尝试获取互斥锁但未成功时,它会反复检查互斥锁的状态直到成功获取为止。这种方式避免了线程切换的开销,适用于短暂的争用情况。 除了自旋锁,还有其他形式的互斥锁实现,如基于信号量、互斥体等。这些实现方式依赖于操作系统的支持,通过系统调用来实现对锁的操作。 总的来说,互斥锁的底层实现通常依赖于硬件指令或操作系统提供的原子操作,以确保对共享资源的访问是原子的,并通过自旋等方式提供线程之间的同步机制。具体的实现方式可能因操作系统或硬件平台而异。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值