Go语言并发编程中RwMutex 注意事项

发布于 1周前 作者 itying888 最后一次编辑是 5天前 来自 问答

Go语言并发编程中RwMutex 注意事项

RWMutex 是单写多读锁,该锁可以加多个读锁或者一个写锁

读锁占用的情况下会阻止写,不会阻止读,多个 Goroutine 可以同时获取 读锁

写锁会阻止其他 Goroutine(无论读和写)进来,整个锁由该 Goroutine 独占

适用于读多写少的场景

RWMutex 类型变量的零值是一个未锁定状态的互斥锁

RWMutex 在首次被使用之后就不能再被拷贝

RWMutex 的读锁或写锁在未锁定状态,解锁操作都会引发 panic

RWMutex 的一个写锁去锁定临界区的共享资源,如果临界区的共享资源已 被(读锁或写锁)锁定,这个写锁操作的 goroutine 将被阻塞直到解锁

RWMutex 的读锁不要用于递归调用,比较容易产生死锁

RWMutex 的锁定状态与特定的 goroutine 没有关联。一个 goroutine 可 以 RLock(Lock),另一个 goroutine 可以 RUnlock(Unlock)

写锁被解锁后,所有因操作锁定读锁而被阻塞的 goroutine 会被唤醒,并 都可以成功锁定读锁

读锁被解锁后,在没有被其他读锁锁定的前提下,所有因操作锁定写锁而 被阻塞的 Goroutine,其中等待时间最长的一个 Goroutine 会被唤醒

1 回复

Go语言并发编程中RWMutex 注意事项

在Go语言的并发编程中,sync.RWMutex 是一个非常有用的同步机制,它允许读操作并发执行,但写操作会互斥进行,以优化读多写少的场景。然而,在使用 RWMutex 时,有几个重要的注意事项需要开发者注意:

1. 避免饥饿问题

饥饿问题是指长时间无法获取到锁的情况,虽然 RWMutex 设计上倾向于让读操作快速通过,但在极端情况下,如果有大量的写操作在等待,读操作可能会长时间阻塞写操作,导致写操作饥饿。

示例代码

var rwMutex sync.RWMutex

// 假设这个函数频繁被调用进行读操作
func ReadData() {
    rwMutex.RLock()
    defer rwMutex.RUnlock()
    // 读取数据操作
}

// 假设这个函数偶尔被调用进行写操作
func WriteData() {
    rwMutex.Lock()
    defer rwMutex.Unlock()
    // 写入数据操作
}

// 注意:在高写频率的场景下,可能需要考虑其他同步机制

2. 确保锁的正确释放

在使用 RWMutex 时,确保在逻辑块的结束处释放锁。通常使用 defer 语句来保证锁的释放,即使发生异常也能保证锁的正确释放。

示例代码(已在上面的示例中展示)

3. 锁的粒度

合理控制锁的粒度对于性能至关重要。过粗的锁粒度会导致不必要的阻塞,而过细的锁粒度又可能导致维护困难和性能下降(如过多的锁竞争)。

示例分析

  • 避免将整个结构体或大型数据结构作为锁的保护对象,而应只锁定需要修改或读取的部分。

4. 避免在持有锁时执行耗时操作

持有锁时执行耗时操作会阻塞其他goroutine的访问,降低系统的并发性能。

示例代码(错误示例):

rwMutex.Lock()
defer rwMutex.Unlock()

// 假设这是一个耗时的网络请求
time.Sleep(1 * time.Second)

// 写入数据

改进示例

// 可以在锁外进行耗时操作,仅在必要时加锁
result := fetchDataFromNetwork()
rwMutex.Lock()
defer rwMutex.Unlock()

// 写入从网络获取的数据

5. 注意读写锁的升级与降级

Go的 RWMutex 不支持读写锁的升级(从读锁到写锁)和降级(从写锁到读锁),尝试这样做会导致死锁。

示例说明

  • 升级锁(死锁风险):从读锁到写锁的转换是不被允许的,因为这会阻塞所有后续的读操作,包括已经持有读锁的当前goroutine自己尝试获取写锁。
  • 降级锁(同样风险):虽然技术上可以在逻辑上实现(先释放读锁,再获取写锁,然后立即释放写锁,重新获取读锁),但这增加了代码的复杂性和出错的风险。

总之,在使用 sync.RWMutex 时,开发者应仔细考虑上述注意事项,以确保程序的正确性和高效性。

回到顶部