文章目录

Go 互斥锁:sync.Mutex 与 sync.RWMutex

发布于 2026-04-12 11:23:28 · 浏览 10 次 · 评论 0 条

Go 互斥锁:sync.Mutex 与 sync.RWMutex

并发编程中,多个协程同时访问共享数据会导致数据竞争。Go 语言提供了 sync.Mutexsync.RWMutex 两种锁机制来解决这个问题。以下指南将直接展示如何选择和使用这两种锁。


1. 基础互斥锁:sync.Mutex

sync.Mutex 是最基础的锁,它就像一个单间的洗手间,任何时刻只允许一个人使用。

使用步骤

  1. 定义 锁变量。
  2. 访问 共享资源前,调用 mu.Lock()
  3. 操作 完成后,调用 mu.Unlock() 释放锁。
  4. 搭配 defer 语句,确保锁在任何情况下(包括发生错误时)都能被释放。

代码示例

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    mu  sync.Mutex
    val int
}

func (c *Counter) Increment() {
    // 加锁
    c.mu.Lock()
    // 使用 defer 确保函数退出时解锁
    defer c.mu.Unlock()

    // 临界区:安全操作共享数据
    c.val++
}

func main() {
    var wg sync.WaitGroup
    c := Counter{val: 0}

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Increment()
        }()
    }

    wg.Wait()
    fmt.Println("Final Value:", c.val)
}

2. 读写锁:sync.RWMutex

sync.RWMutex 区分了读操作和写操作。它允许多个协程同时读取数据,但在写入数据时会阻止所有其他读写操作。适用于“读多写少”的场景。

核心逻辑

以下流程展示了读写锁的决策逻辑:

graph TD Start["开始: 请求访问资源"] --> Check{操作类型} Check -- "写操作" --> WriteLock["调用: Lock"] Check -- "读操作" --> ReadLock["调用: RLock"] WriteLock --> WriteCrit["独占临界区: 阻塞其他所有读写"] ReadLock --> ReadCrit["共享临界区: 仅阻塞写操作"] WriteCrit --> WriteUnlock["调用: Unlock"] ReadCrit --> ReadUnlock["调用: RUnlock"] WriteUnlock --> End["结束"] ReadUnlock --> End

使用步骤

  1. 定义 sync.RWMutex 变量。
  2. 执行 读操作时,调用 mu.RLock(),结束后 调用 mu.RUnlock()
  3. 执行 写操作时,调用 mu.Lock(),结束后 调用 mu.Unlock()

代码示例

package main

import (
    "fmt"
    "sync"
    "time"
)

type DataStore struct {
    rwmu sync.RWMutex
    data map[string]string
}

func (ds *DataStore) Read(key string) string {
    // 加读锁:允许并发读,阻塞写
    ds.rwmu.RLock()
    defer ds.rwmu.RUnlock()

    // 模拟耗时读操作
    time.Sleep(10 * time.Millisecond)
    return ds.data[key]
}

func (ds *DataStore) Write(key, value string) {
    // 加写锁:阻塞所有读写
    ds.rwmu.Lock()
    defer ds.rwmu.Unlock()

    // 模拟耗时写操作
    time.Sleep(20 * time.Millisecond)
    ds.data[key] = value
}

func main() {
    ds := DataStore{data: make(map[string]string)}
    var wg sync.WaitGroup

    // 启动 10 个读协程
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Reader %d: %s\n", id, ds.Read("test"))
        }(i)
    }

    // 启动 1 个写协程
    wg.Add(1)
    go func() {
        defer wg.Done()
        ds.Write("test", "new_value")
    }()

    wg.Wait()
}

3. 对比与选择:Mutex 还是 RWMutex?

根据实际业务场景选择合适的锁。以下是两者的核心区别:

特性 sync.Mutex sync.RWMutex
读写关系 读写完全互斥,串行执行 读读并发,读写互斥,写写互斥
性能开销 开销较低,逻辑简单 读操作虽并发,但锁内部维护状态有额外开销
适用场景 写操作频繁,或读写频率均衡 读操作非常频繁,写操作极少

4. 关键注意事项

在实际开发中,必须严格遵守以下规则以避免 Bug 和死锁。

禁止复制锁

sync.Mutexsync.RWMutex 包含内部状态字段。如果将其作为值变量复制,内部状态会丢失,导致锁失效或死机。

  1. 传递 锁时,必须使用 指针 *sync.Mutex
  2. 结构体 中若有锁字段,必须通过 指针接收者方法来操作。
// 错误示例:锁被复制
type BadStruct struct {
    mu sync.Mutex
}

// 正确示例:使用指针
type GoodStruct struct {
    mu sync.Mutex
}

func (g *GoodStruct) SafeMethod() {
    g.mu.Lock()
    defer g.mu.Unlock()
}

避免重入

Go 的锁是“非重入锁”。同一个协程不能连续多次锁定同一个未解锁的锁,否则会永久阻塞(死锁)。

  1. 检查 代码逻辑,确保没有递归调用锁。
  2. 避免 在持有锁时调用外部可能再次尝试获取该锁的函数。

锁的粒度

锁的范围(临界区)越大,并发性能越差。

  1. 锁定 仅保护真正的共享数据操作。
  2. 耗时的非共享操作(如网络请求、日志打印)移出 锁的范围。
// 优化示例
func Process() {
    mu.Lock()
    data := sharedData // 仅快速读取共享数据
    mu.Unlock()        // 尽早解锁

    // 执行耗时操作,此时不持有锁,其他协程可以获取锁
    result := heavyCalculation(data)

    mu.Lock()
    sharedData = result // 再次加锁写入
    mu.Unlock()
}

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文