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

avatar
作者
猴君
阅读量:0

✍个人博客:Pandaconda-CSDN博客

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

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

4. R WMutex 实现

sync.RWMutex 是 Go 语言标准库提供的一种读写锁,用于在多个 goroutine 同时访问共享资源时进行保护。与 sync.Mutex 类似,sync.RWMutex 也是通过互斥锁实现的,但是它允许多个 goroutine 同时获取读锁,而只允许一个 goroutine 获取写锁。

下面是一个简单的 sync.RWMutex 的实现示例:

type RWMutex struct {     writerSem    chan struct{} // 写者用的信号量     readerSem    chan struct{} // 读者用的信号量     readerCount  int           // 当前持有读锁的goroutine数量     writerCount  int           // 当前持有写锁的goroutine数量     readerWait   int           // 正在等待读锁的goroutine数量     writerWait   int           // 正在等待写锁的goroutine数量     writerLocked bool          // 是否有goroutine持有写锁 } func NewRWMutex() *RWMutex {     return &RWMutex{         writerSem: make(chan struct{}, 1),         readerSem: make(chan struct{}, 1),     } } // 获取读锁 func (m *RWMutex) RLock() {     // 获取读锁的过程需要加锁     m.writerSem <- struct{}{} // 防止写者获取锁     m.readerSem <- struct{}{} // 获取读锁的信号量     // 更新状态     m.readerCount++     if m.writerLocked || m.writerWait > 0 {         m.readerWait++         <-m.readerSem // 等待写者释放锁         m.readerWait--     }     // 释放加锁时获取的信号量     <-m.writerSem } // 释放读锁 func (m *RWMutex) RUnlock() {     // 获取读锁的过程需要加锁     m.writerSem <- struct{}{} // 防止写者获取锁     // 更新状态     m.readerCount--     if m.readerCount == 0 && m.writerWait > 0 {         <-m.writerSem // 优先唤醒写者     }     // 释放加锁时获取的信号量     <-m.writerSem } // 获取写锁 func (m *RWMutex) Lock() {     // 获取写锁的过程需要加锁     m.writerSem <- struct{}{} // 防止其他写者获取锁     m.writerWait++     // 等待其他goroutine释放读锁或写锁     for m.writerLocked || m.readerCount > 0 {         <-m.readerSem     }     // 更新状态     m.writerWait--     m.writerLocked = true     // 释放加锁时获取的信号量     <-m.writerSem } // 释放写锁 func (m *RWMutex) Unlock() {     // 获取写锁的过程需要加锁     m.writerSem <- struct{}{}     // 更新状态     m.writerLocked = false     if m.writerWait > 0 {         <-m.writerSem     } else if m.readerWait > 0 {         for i := 0; i < m.readerCount; i++ {             m.readerSem <- struct{}{} // 优先唤醒读者         }     }     // 释放加锁时获取的信号量     <-m.writerSem }

在这个实现中,sync.RWMutex 包含以下成员:

  • writerSemreaderSem:两个用于同步的信号量通道。写锁会在 writerSem 上等待,读锁会在 readerSem 上等待。

  • readerCountwriterCount:当前持有读锁和写锁的 goroutine 数量。

  • readerWaitwriterWait:正在等待读锁和写锁的 goroutine 数量。

  • writerLocked:标记当前是否有 goroutine 持有写锁。

在读锁和写锁获取和释放的过程中,都需要先获取 writerSem 信号量防止其他写者获取锁。获取读锁时还需要获取 readerSem 信号量,而获取写锁时需要等待其他 goroutine 释放读锁或写锁。

这个实现中有两个重要的细节:

  • 优先唤醒写者:在释放读锁或写锁时,如果有正在等待的写锁 goroutine,应该优先唤醒它们,因为写锁的优先级更高。

  • 读锁的等待问题:在等待读锁的 goroutine 中,如果有其他 goroutine 正在持有写锁或等待写锁,那么这些读锁 goroutine 应该等待写锁 goroutine 释放锁,避免因等待读锁而导致写锁饥饿。

5. R WMutex 注意事项

  • RWMutex 是单写多读锁,该锁可以加多个读锁或者一个写锁。

  • 读锁占用的情况下会阻止写,不会阻止读,多个 Goroutine 可以同时获取读锁。

  • 写锁会阻止其他 Goroutine(无论读和写)进来,整个锁由该 Goroutine 独占。

  • 适用于读多写少的场景。

  • RWMutex 类型变量的零值是一个未锁定状态的互斥锁。

  • RWMutex 在首次被使用之后就不能再被拷贝。

  • RWMutex 的读锁或写锁在未锁定状态,解锁操作都会引发 panic。

  • RWMutex 的一个写锁去锁定临界区的共享资源,如果临界区的共享资源已被(读锁或写锁)锁定,这个写锁操作的 goroutine 将被阻塞直到解锁。

  • RWMutex 的读锁不要用于递归调用,比较容易产生死锁。

  • RWMutex 的锁定状态与特定的 goroutine 没有关联。一个 goroutine 可以 RLock(Lock),另一个 goroutine 可以 RUnlock(Unlock)。

  • 写锁被解锁后,所有因操作锁定读锁而被阻塞的 goroutine 会被唤醒,并都可以成功锁定读锁。

  • 读锁被解锁后,在没有被其他读锁锁定的前提下,所有因操作锁定写锁而被阻塞的 Goroutine,其中等待时间最长的一个 Goroutine 会被唤醒。

 6. Go 读写锁的实现原理?

概念:

读写互斥锁 RWMutex,是对 Mutex 的一个扩展,当一个 goroutine 获得了读锁后,其他 goroutine 可以获取读锁,但不能获取写锁;当一个 goroutine 获得了写锁后,其他 goroutine 既不能获取读锁也不能获取写锁(只能存在一个写者或多个读者,可以同时读)。

使用场景:

多于的情况(既保证线程安全,又保证性能不太差)。

底层实现结构:

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

type RWMutex struct {     w           Mutex  // 复用互斥锁     writerSem   uint32 // 信号量,用于写等待读     readerSem   uint32 // 信号量,用于读等待写     readerCount int32  // 当前执行读的 goroutine 数量     readerWait  int32  // 被阻塞的准备读的 goroutine 的数量 }

操作:

读锁的加锁与释放

func (rw *RWMutex) RLock() // 加读锁 func (rw *RWMutex) RUnlock() // 释放读锁

加读锁

func (rw *RWMutex) RLock() { // 为什么readerCount会小于0呢?往下看发现writer的Lock()会对readerCount做减法操作(原子操作)   if atomic.AddInt32(&rw.readerCount, 1) < 0 {     // A writer is pending, wait for it.     runtime_Semacquire(&rw.readerSem)   } }

atomic.AddInt32(&rw.readerCount, 1) 调用这个原子方法,对当前在读的数量加 1,如果返回负数,那么说明当前有其他写锁,这时候就调用 runtime_SemacquireMutex 休眠当前 goroutine 等待被唤醒。

释放读锁

解锁的时候对正在读的操作减 1,如果返回值小于 0 那么说明当前有在写的操作,这个时候调用 rUnlockSlow 进入慢速通道。

func (rw *RWMutex) RUnlock() {     if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {         rw.rUnlockSlow(r)     } }

被阻塞的准备读的 goroutine 的数量减 1,readerWait 为 0,就表示当前没有正在准备读的 goroutine 这时候调用 runtime_Semrelease 唤醒写操作。

func (rw *RWMutex) rUnlockSlow(r int32) {     // A writer is pending.     if atomic.AddInt32(&rw.readerWait, -1) == 0 {         // The last reader unblocks the writer.         runtime_Semrelease(&rw.writerSem, false, 1)     } }

写锁的加锁与释放。

func (rw *RWMutex) Lock() // 加写锁 func (rw *RWMutex) Unlock() // 释放写锁

加写锁

const rwmutexMaxReaders = 1 << 30 func (rw *RWMutex) Lock() {     // First, resolve competition with other writers.     rw.w.Lock()     // Announce to readers there is a pending writer.     r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders     // Wait for active readers.     if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {         runtime_Semacquire(&rw.writerSem)     } }

首先调用互斥锁的 lock,获取到互斥锁之后,如果计算之后当前仍然有其他 goroutine 持有读锁,那么就调用 runtime_SemacquireMutex 休眠当前的 goroutine 等待所有的读操作完成

这里readerCount 原子性加上一个很大的负数,是防止后面的协程能拿到读锁,阻塞读

释放写锁

func (rw *RWMutex) Unlock() {     // Announce to readers there is no active writer.     r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)     // Unblock blocked readers, if any.     for i := 0; i < int(r); i++ {         runtime_Semrelease(&rw.readerSem, false)     }     // Allow other writers to proceed.     rw.w.Unlock() }

解锁的操作,会先调用 atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders) 将恢复之前写入的负数,然后根据当前有多少个读操作在等待,循环唤醒

注意点:

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

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

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

互斥锁和读写锁的区别:

  • 读写锁区分读者和写者,而互斥锁不区分。

  • 互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!