MST

星途 面试题库

面试题:Go互斥锁在极端高并发场景下的优化策略

设想一个极端高并发场景,有数千个goroutine频繁地竞争互斥锁。在这种情况下,原生的`sync.Mutex`可能会出现性能瓶颈。请你提出至少两种优化策略来提升性能,解释每种策略的原理,并给出相应的代码实现示例,同时分析这些优化策略可能带来的潜在问题。
46.3万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

策略一:使用sync.RWMutex进行读写分离

  • 原理:如果大部分操作是读操作,写操作较少,可以使用读写锁。读操作可以同时进行,只有写操作需要独占锁,这样可以提高并发性能。
  • 代码示例
package main

import (
    "fmt"
    "sync"
)

var (
    data int
    rwmu sync.RWMutex
)

func read(wg *sync.WaitGroup) {
    defer wg.Done()
    rwmu.RLock()
    fmt.Printf("Read data: %d\n", data)
    rwmu.RUnlock()
}

func write(wg *sync.WaitGroup) {
    defer wg.Done()
    rwmu.Lock()
    data++
    fmt.Printf("Write data: %d\n", data)
    rwmu.Unlock()
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go read(&wg)
    }
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go write(&wg)
    }
    wg.Wait()
}
  • 潜在问题:如果写操作频繁,读操作也会因为写锁的独占而等待,反而降低性能。同时,读写锁的实现比普通互斥锁复杂,可能会增加维护成本。

策略二:使用分段锁

  • 原理:将数据分成多个段,每个段使用一个独立的互斥锁。这样不同段的数据操作可以并发进行,减少锁竞争。
  • 代码示例
package main

import (
    "fmt"
    "sync"
)

const numSegments = 10

type Segment struct {
    data int
    mu   sync.Mutex
}

type ShardedMutex struct {
    segments [numSegments]Segment
}

func (sm *ShardedMutex) getSegmentIndex(key int) int {
    return key % numSegments
}

func (sm *ShardedMutex) Lock(key int) {
    index := sm.getSegmentIndex(key)
    sm.segments[index].mu.Lock()
}

func (sm *ShardedMutex) Unlock(key int) {
    index := sm.getSegmentIndex(key)
    sm.segments[index].mu.Unlock()
}

func worker(sm *ShardedMutex, key, value int, wg *sync.WaitGroup) {
    defer wg.Done()
    sm.Lock(key)
    sm.segments[sm.getSegmentIndex(key)].data += value
    fmt.Printf("Worker with key %d updated data: %d\n", key, sm.segments[sm.getSegmentIndex(key)].data)
    sm.Unlock(key)
}

func main() {
    var wg sync.WaitGroup
    sm := &ShardedMutex{}
    for i := 0; i < 20; i++ {
        wg.Add(1)
        go worker(sm, i, i, &wg)
    }
    wg.Wait()
}
  • 潜在问题:需要合理设计分段逻辑,如果分段不合理,可能仍然会存在较高的锁竞争。同时,增加了代码的复杂性,需要管理多个锁。

策略三:减少锁的粒度

  • 原理:尽量缩小锁保护的代码块范围,只在真正需要保护共享资源的代码部分加锁,减少锁的持有时间,从而降低锁竞争。
  • 代码示例
package main

import (
    "fmt"
    "sync"
)

var (
    data int
    mu   sync.Mutex
)

func updateData(wg *sync.WaitGroup) {
    defer wg.Done()
    localData := 0
    // 只在读取和更新共享数据时加锁
    mu.Lock()
    localData = data
    localData++
    data = localData
    mu.Unlock()
    fmt.Printf("Updated data: %d\n", data)
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go updateData(&wg)
    }
    wg.Wait()
}
  • 潜在问题:缩小锁粒度可能会导致代码逻辑分散,增加代码的维护难度。同时,如果缩小过度,可能无法正确保护共享资源,导致数据竞争问题。