MST

星途 面试题库

面试题:如何在高并发场景下平衡Go语言Mutex锁的公平性与性能

假设你正在开发一个高并发的Go程序,其中有大量的goroutine频繁竞争Mutex锁。描述你会采取哪些策略来在保证一定公平性的前提下,尽可能优化性能?从锁的初始化设置、业务逻辑设计、以及可能用到的其他同步工具等方面阐述。
26.9万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

锁的初始化设置

  1. 使用sync.Mutex 或 sync.RWMutex
    • sync.Mutex:在大多数简单场景下,普通的sync.Mutex就足够。如果读操作远远多于写操作,可以考虑sync.RWMutexRWMutex允许多个读操作同时进行,只有写操作时才会独占锁,这样能提高读操作的并发性能。但要注意,写操作时会阻塞所有读操作和其他写操作。
    • 初始化示例
var mu sync.Mutex
var rwmu sync.RWMutex
  1. 公平锁设置:Go 1.9 及以上版本,sync.Mutex提供了公平锁模式。可以通过设置内部状态实现公平锁。在高并发竞争场景下,公平锁能保证等待时间最长的 goroutine 优先获取锁,减少饥饿现象。
    • 示例
var mu sync.Mutex
// 模拟获取公平锁
mu.Lock()
defer mu.Unlock()

在公平锁模式下,新的 goroutine 在获取锁时会先检查是否有其他 goroutine 等待锁的时间比自己长,如果有则等待。

业务逻辑设计

  1. 减少锁的粒度
    • 尽量将大的临界区拆分成多个小的临界区。例如,如果一个结构体有多个字段,而不同的操作只涉及部分字段,可以为每个字段或者相关字段组分别设置锁。
    • 示例
type Data struct {
    field1 int
    field2 string
    mu1    sync.Mutex
    mu2    sync.Mutex
}

func (d *Data) updateField1() {
    d.mu1.Lock()
    defer d.mu1.Unlock()
    d.field1++
}

func (d *Data) updateField2() {
    d.mu2.Lock()
    defer d.mu2.Unlock()
    d.field2 = "new value"
}
  1. 优化锁的持有时间
    • 尽量缩短持有锁的时间。在持有锁期间,只执行必要的操作,避免在锁内进行复杂的计算、I/O 操作等。
    • 示例
var mu sync.Mutex
var data []int

func processData() {
    // 提前准备数据
    var localData []int
    {
        mu.Lock()
        localData = make([]int, len(data))
        copy(localData, data)
        mu.Unlock()
    }
    // 这里对 localData 进行复杂计算,不占用锁
    for i := range localData {
        localData[i] = localData[i] * 2
    }
    // 写回数据时再获取锁
    mu.Lock()
    data = localData
    mu.Unlock()
}
  1. 读写分离
    • 如果业务场景允许,将读操作和写操作分开处理。读操作可以并发执行,写操作则独占锁。使用sync.RWMutex来实现这种读写分离的场景。
    • 示例
var rwmu sync.RWMutex
var data int

func readData() int {
    rwmu.RLock()
    defer rwmu.RUnlock()
    return data
}

func writeData(newData int) {
    rwmu.Lock()
    defer rwmu.Unlock()
    data = newData
}

其他同步工具

  1. sync.Cond
    • sync.Cond可以用于在多个 goroutine 之间进行条件同步。当某个条件满足时,通知等待的 goroutine。可以结合Mutex使用,在需要等待某个条件的场景下,避免不必要的锁竞争。
    • 示例
var mu sync.Mutex
var cond = sync.NewCond(&mu)
var ready bool

func worker() {
    mu.Lock()
    for!ready {
        cond.Wait()
    }
    // 执行工作
    mu.Unlock()
}

func activator() {
    mu.Lock()
    ready = true
    cond.Broadcast()
    mu.Unlock()
}
  1. sync.Map
    • 在高并发读写场景下,如果需要使用 map 结构,可以使用sync.Map。它内部实现了锁分离等优化机制,能在高并发下提供较好的性能,并且无需用户手动加锁。
    • 示例
var syncMap sync.Map

func set(key string, value int) {
    syncMap.Store(key, value)
}

func get(key string) (int, bool) {
    val, ok := syncMap.Load(key)
    if!ok {
        return 0, false
    }
    return val.(int), true
}
  1. channel
    • 使用 channel 进行数据传递和同步。通过 channel 可以实现 goroutine 之间的解耦,避免使用锁来保护共享数据。例如,可以使用 buffered channel 来缓存数据,生产者和消费者通过 channel 进行数据传递,而不是直接操作共享数据。
    • 示例
func producer(ch chan int) {
    for i := 0; i < 10; i++ {
        ch <- i
    }
    close(ch)
}

func consumer(ch chan int) {
    for val := range ch {
        // 处理数据
        println(val)
    }
}