Skip to content

RWMutex

Go sync.RWMutex 详解

sync.RWMutex 是 Go 并发编程中的一种锁,它支持读写分离的方式,允许多个 Goroutine 同时读取共享资源,但在写入时只能有一个 Goroutine 操作。RWMutex 是基于 sync.Mutex 的扩展,提供了更多的灵活性,尤其适用于读多写少的场景。

1. 基本概念

  • 读锁 (RLock()):允许多个 Goroutine 同时获取读锁,只有在没有写锁时才能获取。多个 Goroutine 可以同时读取共享资源。
  • 写锁 (Lock()):只有一个 Goroutine 可以获取写锁,且在有任何读锁或写锁的情况下,其他 Goroutine 无法获取写锁。写锁是独占的。

2. RWMutex 的方法

  • RLock():获取读锁,允许多个 Goroutine 同时获得,但只有在没有写锁时才会成功获取。
  • RUnlock():释放读锁。
  • Lock():获取写锁,独占锁定资源,只有一个 Goroutine 可以获取写锁。
  • Unlock():释放写锁。

3. RWMutex 的使用示例

下面是一个常见的使用 RWMutex 的场景,多个 Goroutine 同时读取共享资源,少数 Goroutine 进行写入操作。

go
package main

import (
    "fmt"
    "sync"
)

var (
    mu      sync.RWMutex  // 读写锁
    counter int
)

func read() int {
    mu.RLock()   // 获取读锁
    defer mu.RUnlock() // 确保在函数结束时释放读锁
    return counter
}

func write() {
    mu.Lock()   // 获取写锁
    defer mu.Unlock() // 确保在函数结束时释放写锁
    counter++
}

func main() {
    var wg sync.WaitGroup

    // 启动多个 Goroutine 同时读和写共享变量
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            write()  // 写入操作
        }()
    }
    
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            read()  // 读取操作
        }()
    }

    wg.Wait()
    fmt.Println("Counter:", counter)
}

输出

Counter: 1000

在这个示例中:

  • 通过 RLock() 来读取共享变量 counter,多个 Goroutine 可以同时执行读取操作。
  • 通过 Lock() 来写入 counter,每次只能有一个 Goroutine 执行写入操作。
  • 由于使用了 RWMutex,多个读取操作是并发进行的,而写入操作是排他的。

4. RWMutexMutex 的对比

特性sync.Mutexsync.RWMutex
锁类型独占锁读锁(多个 Goroutine 可共享)、写锁(独占)
并发性只能一个 Goroutine 获取锁读锁时可以多个 Goroutine 同时获取
性能在写操作较多时更高效读操作较多时更高效,避免写锁竞争
适用场景读写操作相对均衡读多写少的场景,如缓存、配置等共享资源

5. 何时使用 RWMutex

RWMutex 适用于以下场景:

  • 读多写少的场景:当大多数操作是读取共享资源,而写操作较少时,使用 RWMutex 可以提高性能,因为多个 Goroutine 可以并发读取数据。
  • 需要提高并发度:当多个 Goroutine 需要频繁读取相同的共享资源时,RWMutex 允许多个 Goroutine 同时进行读取。

6. 注意事项

  • 避免死锁:在使用 RWMutex 时,应该遵循获取锁的顺序,确保不会因写锁和读锁之间的死锁而导致问题。
  • 避免过度锁定:尽量减小锁定区域,减少不必要的锁定时间,避免影响并发性能。
  • 读锁和写锁不能混用:在同一个 Goroutine 中,不能在持有写锁的情况下再次获取读锁,或者反之。必须先释放一个锁后再获取另一个锁。

7. 写操作的排他性

RWMutex 中,写锁是独占的,意味着一旦一个 Goroutine 获得了写锁,其他任何 Goroutine 都无法获得写锁或读锁,直到写锁被释放。

go
package main

import (
    "fmt"
    "sync"
)

var (
    mu      sync.RWMutex
    counter int
)

func write() {
    mu.Lock()   // 获取写锁
    defer mu.Unlock()
    counter++
}

func main() {
    var wg sync.WaitGroup

    // 启动 3 个写操作 Goroutine
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            write()
        }()
    }

    // 启动 3 个读取操作 Goroutine
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.RLock()   // 获取读锁
            fmt.Println("Reading counter:", counter)
            mu.RUnlock() // 释放读锁
        }()
    }

    wg.Wait()
    fmt.Println("Final counter:", counter)
}

8. 总结

  • sync.RWMutex 是 Go 中提供的一种读写锁,它通过分离读锁和写锁来提升读多写少的并发性能。
  • 读锁 (RLock()) 允许多个 Goroutine 同时读取共享资源,而写锁 (Lock()) 是独占的,保证只有一个 Goroutine 可以修改资源。
  • 使用 RWMutex 可以提高程序的并发性能,特别是在读操作较多而写操作较少的场景下。
  • 在使用 RWMutex 时,必须小心死锁和锁的使用顺序,确保锁的正确释放。