写点什么

共享资源的保护:使用 RWMutex 的正确姿势

作者:Jack
  • 2023-04-06
    广东
  • 本文字数:3393 字

    阅读完需:约 11 分钟

共享资源的保护:使用RWMutex的正确姿势

在多线程编程中,保证共享资源的一致性是一个非常重要的问题。为了解决这个问题,Go 语言提供了一种称为 RWMutex 的同步机制,它可以用来保护共享资源,在不同场景下,它可以用来读写锁,也可以用来普通的互斥锁。在本文中,我们将深入探讨 RWMutex 的原理、使用方法和注意事项。

一、RWMutex 的原理


RWMutex 的实现基于 sync.Mutex,它包含一个 Mutex 和两个计数器,分别用来记录 read 和 write 的数量。当一个 goroutine 试图获取 RWMutex 的 read 锁或 write 锁时,它首先需要获取 Mutex 的锁,然后检查当前 RWMutex 的状态,如果 RWMutex 处于 write 状态,则 goroutine 需要等待,否则它可以获得 read 或 write 锁。在 read 状态下,多个 goroutine 可以同时读取共享资源,而在 write 状态下,只有一个 goroutine 能够写入共享资源。

1.RLock/RUnlock 的实现

func (rw *RWMutex) RLock() {	if race.Enabled {		_ = rw.w.state		race.Disable()	}	if rw.readerCount.Add(1) < 0 {		// A writer is pending, wait for it.		runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)	}	if race.Enabled {		race.Enable()		race.Acquire(unsafe.Pointer(&rw.readerSem))	}}

func (rw *RWMutex) RUnlock() { if race.Enabled { _ = rw.w.state race.ReleaseMerge(unsafe.Pointer(&rw.writerSem)) race.Disable() } if r := rw.readerCount.Add(-1); r < 0 { // Outlined slow-path to allow the fast-path to be inlined rw.rUnlockSlow(r) } if race.Enabled { race.Enable() }}
func (rw *RWMutex) rUnlockSlow(r int32) { if r+1 == 0 || r+1 == -rwmutexMaxReaders { race.Enable() fatal("sync: RUnlock of unlocked RWMutex") } // A writer is pending. if rw.readerWait.Add(-1) == 0 { // The last reader unblocks the writer. runtime_Semrelease(&rw.writerSem, false, 1) }}
复制代码


  • 没有 writer 竞争或持有锁时,readerCount 和我们正常理解的 reader 的计数是一样的;

  • 如果有 writer 竞争锁或者持有锁时,那么,readerCount 不仅仅承担着 reader 的计数功能,还能够标识当前是否有 writer 竞争或持有锁,在这种情况下,请求锁的 reader 的处理进入第 8 行,阻塞等待锁的释放。


调用 RUnlock 的时候,我们需要将 Reader 的计数减去 1(第 22 行),因为 reader 的数量减少了一个。但是,第 23 行的 Add 的返回值还有另外一个含义。


如果它是负值,就表示当前有 writer 竞争锁,在这种情况下,还会调用 rUnlockSlow 方法,检查是不是 reader 都释放读锁了,如果读锁都释放了,那么可以唤醒请求写锁的 writer 了。


当一个或者多个 reader 持有锁的时候,竞争锁的 writer 会等待这些 reader 释放完,才可能持有这把锁。

例如,在房地产行业中有条规矩叫做“买卖不破租赁”,意思是说,就算房东把房子卖了,新业主也不能把当前的租户赶走,而是要等到租约结束后,才能接管房子。这和 RWMutex 的设计是一样的。


当 writer 请求锁的时候,是无法改变既有的 reader 持有锁的现实的,也不会强制这些 reader 释放锁,它的优先权只是限定后来的 reader 不要和它抢。所以,rUnlockSlow 将持有锁的 reader 计数减少 1 的时候,会检查既有的 reader 是不是都已经释放了锁,如果都释放了锁,就会唤醒 writer,让 writer 持有锁。


2.Lock/Unlock 的实现

// Lock locks rw for writing.// If the lock is already locked for reading or writing,// Lock blocks until the lock is available.func (rw *RWMutex) Lock() {	if race.Enabled {		_ = rw.w.state		race.Disable()	}	// First, resolve competition with other writers.	rw.w.Lock()	// Announce to readers there is a pending writer.	r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders	// Wait for active readers.	if r != 0 && rw.readerWait.Add(r) != 0 {		runtime_SemacquireRWMutex(&rw.writerSem, false, 0)	}	if race.Enabled {		race.Enable()		race.Acquire(unsafe.Pointer(&rw.readerSem))		race.Acquire(unsafe.Pointer(&rw.writerSem))	}}
复制代码


这段代码是 RWMutex 的 Lock 方法的实现。该方法用于获取写锁,如果锁已经被其他 goroutine 获取,则当前 goroutine 会被阻塞,直到锁可用。


具体来说,该方法首先会获取写锁,然后将读锁的数量减去最大值,并将结果与最大值相加,得到的结果表示当前有多少个读锁。接着,如果有读锁存在,当前 goroutine 会等待所有读锁结束。最后,如果获取写锁成功,该方法会返回。


该方法的实现是基于信号量的。当有多个 goroutine 等待读锁时,它们会在读锁上等待。当有一个 goroutine 等待写锁时,它会在写锁上等待。这种实现方式可以保证读写锁的公平性,避免写锁饥饿。


// Unlock unlocks rw for writing. It is a run-time error if rw is// not locked for writing on entry to Unlock.//// As with Mutexes, a locked RWMutex is not associated with a particular// goroutine. One goroutine may RLock (Lock) a RWMutex and then// arrange for another goroutine to RUnlock (Unlock) it.func (rw *RWMutex) Unlock() {	if race.Enabled {		_ = rw.w.state		race.Release(unsafe.Pointer(&rw.readerSem))		race.Disable()	}
// Announce to readers there is no active writer. r := rw.readerCount.Add(rwmutexMaxReaders) if r >= rwmutexMaxReaders { race.Enable() fatal("sync: Unlock of unlocked RWMutex") } // Unblock blocked readers, if any. for i := 0; i < int(r); i++ { runtime_Semrelease(&rw.readerSem, false, 0) } // Allow other writers to proceed. rw.w.Unlock() if race.Enabled { race.Enable() }}
复制代码


这段代码是 RWMutex 的 Unlock 方法的实现。该方法用于释放写锁,如果锁未被获取,则会触发运行时错误。


具体来说,该方法会将读锁的数量加上最大值,并检查结果是否超过最大值。如果超过最大值,则说明锁未被获取。否则,该方法会释放所有等待读锁的 goroutine,并释放写锁。在释放读锁时,该方法会依次释放所有等待读锁的 goroutine。这种实现方式可以保证读写锁的公平性,避免写锁饥饿。


需要注意的是,RWMutex 的锁不与特定的 goroutine 相关联。一个 goroutine 可以获取 RWMutex 的读锁或写锁,并安排另一个 goroutine 来释放它。

二、RWMutex 的使用方法


在使用 RWMutex 时,需要根据具体的场景选择不同的读写锁使用方式。以下是几个使用 RWMutex 的场景及相应的读写锁使用方式:


  1. 多个 goroutine 同时读取共享资源:在这种情况下,可以使用读锁来提高并发性能。多个 goroutine 可以同时获得读锁,但只有一个 goroutine 可以获得写锁。例如:

func foo() {    rwMutex.RLock()    defer rwMutex.RUnlock()    // do something}
复制代码
  1. 多个 goroutine 同时写入共享资源:在这种情况下,需要使用写锁来保证共享资源的一致性。只有一个 goroutine 可以获得写锁,其他 goroutine 需要等待。例如:

func bar() {    rwMutex.Lock()    defer rwMutex.Unlock()    // do something}
复制代码
  1. 读写锁的使用频率相当:在这种情况下,可以根据具体的情况选择使用读锁或写锁。例如:

func baz() {    if someCondition {        rwMutex.RLock()        defer rwMutex.RUnlock()        // do something    } else {        rwMutex.Lock()        defer rwMutex.Unlock()        // do something    }}
复制代码


三、RWMutex 的注意事项


  1. 在使用 RWMutex 时,需要注意锁的粒度。锁的粒度过大会导致效率低下,锁的粒度过小会导致程序的正确性受到威胁。

  2. 在使用 RWMutex 时,需要注意锁的获取和释放顺序。如果不按照正确的顺序获取和释放锁,会导致死锁。

  3. 在使用 RWMutex 时,需要注意共享资源的一致性。如果共享资源的访问不当,会导致程序的正确性受到威胁。


四、总结


RWMutex 是 Go 语言提供的一种同步机制,它可以用来保护共享资源,在不同场景下,它可以用来读写锁,也可以用来普通的互斥锁。在实际使用 RWMutex 时,需要根据具体的场景选择不同的读写锁使用方式,以提高程序的并发性能和保证共享资源的一致性。同时,需要注意锁的粒度、获取和释放顺序以及共享资源的一致性等问题。

发布于: 2023-04-06阅读数: 15
用户头像

Jack

关注

还未添加个人签名 2019-05-12 加入

作为一名技术追求者,我对科技、编程和创新充满热情。我始终关注最新的商业趋势和技术发展,努力将其应用于实践中,从而推动技术创新和改进。我善于思考和分析,具备较强的解决问题的能力和团队合作精神。

评论

发布
暂无评论
共享资源的保护:使用RWMutex的正确姿势_Jack_InfoQ写作社区