MST
星途 面试题库

面试题:Go语言Mutex与其他同步原语在并发控制中的权衡

在Go语言中,除了Mutex,还有诸如读写锁(RWMutex)、信号量(sync.Semaphore)等同步原语。在一个复杂的分布式系统中,涉及大量的数据读写操作以及资源限制。请详细分析在不同场景下,Mutex与其他同步原语相比的优缺点,并举例说明何时选择Mutex而不是其他同步原语更为合适,反之亦然。
30.8万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

Mutex(互斥锁)

  • 优点
    • 简单直接:实现简单,易于理解和使用。在只需要保证同一时刻只有一个 goroutine 访问共享资源时,Mutex 是最直观的选择。
    • 通用场景:适用于各种读写操作混合且没有明显读写特性区分的场景,对资源访问的控制最为基础和全面。
  • 缺点
    • 性能瓶颈:如果读操作频繁,每次读都要获取 Mutex 锁,会导致读操作串行化,降低系统并发性能。因为同一时刻只允许一个 goroutine 进入临界区,即使是读操作也会相互阻塞。
  • 适用场景
    • 读写混合且无明显特性:例如一个简单的计数器,既有读操作获取当前计数值,也有写操作增加或减少计数值,使用 Mutex 就足够简单有效。
package main

import (
    "fmt"
    "sync"
)

var (
    counter int
    mu      sync.Mutex
)

func increment(wg *sync.WaitGroup) {
    mu.Lock()
    counter++
    mu.Unlock()
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go increment(&wg)
    }
    wg.Wait()
    fmt.Println("Final counter:", counter)
}

RWMutex(读写锁)

  • 优点
    • 读并发:允许多个 goroutine 同时进行读操作,只有写操作时才需要独占锁。当读操作远远多于写操作时,能显著提高系统并发性能。
  • 缺点
    • 复杂:相比 Mutex,使用起来更复杂,需要根据读写操作的特性来正确使用读锁和写锁。
    • 写操作阻塞:写操作时会阻塞所有读操作和其他写操作,可能导致写操作延迟。如果写操作频繁,整体性能会受影响。
  • 适用场景
    • 多读少写:如缓存系统,大量的 goroutine 会读取缓存数据,只有偶尔的更新操作。
package main

import (
    "fmt"
    "sync"
)

var (
    data    = make(map[string]string)
    rwMutex sync.RWMutex
)

func read(key string) string {
    rwMutex.RLock()
    value := data[key]
    rwMutex.RUnlock()
    return value
}

func write(key, value string) {
    rwMutex.Lock()
    data[key] = value
    rwMutex.Unlock()
}

func main() {
    go write("k1", "v1")
    go func() {
        fmt.Println("Read value:", read("k1"))
    }()
}

sync.Semaphore(信号量,Go 1.19+)

  • 优点
    • 资源限制:可以有效限制同时访问共享资源的 goroutine 数量,适用于需要控制并发数的场景,比如数据库连接池,限制同时连接数据库的 goroutine 数量。
  • 缺点
    • 场景特定:功能相对特定,不像 Mutex 那样通用,在不需要控制并发数的场景下使用较为繁琐。
  • 适用场景
    • 资源有限场景:例如限制同时处理的任务数量,防止资源耗尽。
package main

import (
    "context"
    "fmt"
    "sync"
    "time"

    "golang.org/x/sync/semaphore"
)

func main() {
    sem := semaphore.NewWeighted(2)
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            if err := sem.Acquire(context.Background(), 1); err != nil {
                fmt.Println("Acquire error:", err)
                return
            }
            defer sem.Release(1)
            fmt.Printf("Goroutine %d is working\n", id)
            time.Sleep(2 * time.Second)
        }(i)
    }
    wg.Wait()
}

总结:在简单的读写混合场景且对性能要求不是极致的情况下,Mutex 是不错的选择,因其简单易用。而在多读少写的场景下,RWMutex 能大幅提升并发读性能;当需要限制并发访问数量以保护有限资源时,sync.Semaphore 则是合适的选择。