【Golang 面试 - 进阶题】每日 3 题(三)

发布于:2024-07-30 ⋅ 阅读:(57) ⋅ 点赞:(0)

✍个人博客:Pandaconda-CSDN博客

📣专栏地址:http://t.csdnimg.cn/UWz06

📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪

7. Go 互斥锁的实现原理? 

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

概念:

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

使用场景:

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

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

  

底层实现结构:

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

type Mutex struct {
     state int32
     sema  uint32
 }

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

 

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

 

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
    ...
}

操作:

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

加锁

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

  

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

解锁

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

  

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

注意点:

  • 在 Lock() 之前使用 Unlock() 会导致 panic 异常。

  • 使用 Lock() 加锁后,再次 Lock() 会导致死锁(不支持重入),需 Unlock() 解锁后才能再加锁。

  • 锁定状态与 goroutine 没有关联,一个 goroutine 可以 Lock,另一个 goroutine 可以 Unlock。

 8. Go 互斥锁正常模式和饥饿模式的区别?

在 Go 语言中,互斥锁(Mutex)有两种模式:正常模式和饥饿模式。

1、正常模式

在正常模式下,当多个 goroutine 请求锁时,锁会随机地分配给其中的一个 goroutine,其他 goroutine 则会被阻塞。当锁被释放后,等待锁的 goroutine 中的一个会被唤醒,并重新尝试获取锁。

2、饥饿模式

在饥饿模式下,当多个 goroutine 请求锁时,锁会优先分配给等待时间最长的 goroutine,而其他 goroutine 则会被继续阻塞。这种模式下可以避免某些 goroutine 长时间无法获得锁的问题,但是会导致其他 goroutine 无法获得锁的饥饿现象。

在正常情况下,使用正常模式的互斥锁即可满足需求,因为它可以保证所有 goroutine 都有机会获得锁。但是在某些特殊情况下,饥饿模式可能更适合,例如,对于某些实时系统,为了保证某些关键任务及时完成,可能需要使用饥饿模式来保证这些任务获得足够的 CPU 资源。

在 Go 中,默认使用正常模式,但是可以通过在创建互斥锁时设置 Mutex 结构体的 MutexProfile 字段为 MutexProfile{Starvation: true} 来使用饥饿模式。

9. G o 互斥锁允许自旋的条件?

线程没有获取到锁时常见有 2 种处理方式:

  • 一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,这种锁也叫做自旋锁,它不用将线程阻塞起来, 适用于并发低且程序执行时间短的场景,缺点是 cpu 占用较高。

  • 另外一种处理方式就是把自己阻塞起来,会释放 CPU 给其他线程,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒该线程,适用于高并发场景,缺点是有线程上下文切换的开销。

Go 语言中的 Mutex 实现了自旋与阻塞两种场景,当满足不了自旋条件时,就会进入阻塞。

允许自旋的条件:

  1. 锁已被占用,并且锁不处于饥饿模式。

  2. 积累的自旋次数小于最大自旋次数(active_spin=4)。

  3. cpu 核数大于 1。

  4. 有空闲的 P。

  5. 当前 goroutine 所挂载的 P 下,本地待运行队列为空。

if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {  
    ...
    runtime_doSpin()   
    continue  
}


func sync_runtime_canSpin(i int) bool {  
    if i >= active_spin 
    || ncpu <= 1 
    || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {  
          return false  
     }  
   if p := getg().m.p.ptr(); !runqempty(p) {  
      return false  
 }  
   return true  
}

自旋: 

func sync_runtime_doSpin() {
    procyield(active_spin_cnt)
}    

如果可以进入自旋状态之后就会调用 runtime_doSpin 方法进入自旋, doSpin 方法会调用 procyield(30) 执行 30 次 PAUSE 指令,什么都不做,但是会消耗 CPU 时间。 


网站公告

今日签到

点亮在社区的每一天
去签到