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

✍个人博客: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)。

互斥锁和读写锁的区别:

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

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

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值