MST

星途 面试题库

面试题:Go中RWMutex锁实现原理及自定义优化

请深入剖析Go语言RWMutex锁的底层实现原理,包括如何使用信号量或其他机制来管理读写操作的并发控制。如果让你基于现有的RWMutex实现,在不改变其接口的前提下,针对极端读多写少场景进行优化,你会如何设计并实现?
15.9万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

Go语言RWMutex锁底层实现原理

  1. 数据结构
    • RWMutex结构体定义在src/sync/rwmutex.go中,其核心字段如下:
    type RWMutex struct {
        w           Mutex  // 用于写操作的互斥锁
        writerSem   uint32 // 写信号量,用于阻塞写操作
        readerSem   uint32 // 读信号量,用于阻塞读操作
        readerCount int32  // 记录当前活跃的读操作数量
        readerWait  int32  // 记录等待写操作完成的读操作数量
    }
    
  2. 读操作(RLock
    • 首先,readerCount加1。如果readerCount变为负数,说明有写操作正在进行或者等待,此时当前读操作需要阻塞。
    • 通过atomic.AddInt32(&rw.readerCount, 1)增加读操作计数。
    • 如果readerCount为负,调用runtime_SemacquireMutex(&rw.readerSem, false, 0)获取读信号量,进入等待状态,直到写操作完成并释放读信号量。
  3. 读解锁(RUnlock
    • readerCount减1。如果readerCount变为0,说明没有活跃的读操作了,如果有写操作等待(readerWait不为0),则通过runtime_Semrelease(&rw.writerSem, false, 1)释放写信号量,唤醒一个等待的写操作。
    • 通过atomic.AddInt32(&rw.readerCount, -1)减少读操作计数。
  4. 写操作(Lock
    • 首先获取w互斥锁,这一步保证同一时间只有一个写操作可以进入临界区。
    • 然后记录当前活跃的读操作数量readerCountreaderWait,并将readerCount设为负数,这样后续的读操作会被阻塞。
    • 接着循环等待,直到readerCount变为0,即所有读操作都完成,此时调用runtime_SemacquireMutex(&rw.writerSem, true, 0)获取写信号量。
  5. 写解锁(Unlock
    • 释放w互斥锁。
    • readerCount恢复为原来的值(正数),同时通过runtime_Semrelease(&rw.readerSem, true, 0)释放读信号量,唤醒所有等待的读操作。

极端读多写少场景优化设计与实现

  1. 设计思路
    • 采用读写分离的思想,对于读操作,尽量避免与写操作的竞争。可以考虑使用缓存机制,让读操作优先从缓存获取数据,减少对共享资源的直接读取,从而降低读锁的竞争。
    • 对于写操作,采用批量处理的方式,减少写操作的频率。例如,在内存中维护一个写操作队列,当队列达到一定长度或者经过一定时间,再进行一次批量写操作。
  2. 实现示例
    package main
    
    import (
        "container/list"
        "fmt"
        "sync"
        "time"
    )
    
    type RWCache struct {
        cache      map[string]interface{}
        rwMutex    sync.RWMutex
        writeQueue *list.List
        writeTimer *time.Timer
        mutex      sync.Mutex
    }
    
    func NewRWCache() *RWCache {
        rwc := &RWCache{
            cache:      make(map[string]interface{}),
            writeQueue: list.New(),
            writeTimer: time.NewTimer(5 * time.Second),
        }
        go rwc.flushQueue()
        return rwc
    }
    
    func (rwc *RWCache) Get(key string) (interface{}, bool) {
        rwc.rwMutex.RLock()
        defer rwc.rwMutex.RUnlock()
        value, exists := rwc.cache[key]
        return value, exists
    }
    
    func (rwc *RWCache) Set(key string, value interface{}) {
        rwc.mutex.Lock()
        rwc.writeQueue.PushBack(&writeOp{key, value})
        if rwc.writeQueue.Len() >= 10 {
            rwc.flushQueue()
        }
        rwc.mutex.Unlock()
    }
    
    type writeOp struct {
        key   string
        value interface{}
    }
    
    func (rwc *RWCache) flushQueue() {
        for {
            select {
            case <-rwc.writeTimer.C:
                rwc.mutex.Lock()
                for e := rwc.writeQueue.Front(); e != nil; e = e.Next() {
                    op := e.Value.(*writeOp)
                    rwc.rwMutex.Lock()
                    rwc.cache[op.key] = op.value
                    rwc.rwMutex.Unlock()
                }
                rwc.writeQueue.Init()
                rwc.writeTimer.Reset(5 * time.Second)
                rwc.mutex.Unlock()
            }
        }
    }
    
    • 在上述代码中:
      • RWCache结构体包含一个缓存cache,一个读写锁rwMutex,一个写操作队列writeQueue和一个定时器writeTimer
      • Get方法使用读锁来读取缓存数据。
      • Set方法将写操作加入队列,当队列长度达到10或者定时器到期时,进行批量写操作。
      • flushQueue方法在后台运行,负责处理写队列中的操作。这样在极端读多写少场景下,可以有效减少写操作对读操作的影响,提高整体性能。