图解Go语言中的sync.RWMutex:基于多人抢电视遥控器的情景
原文链接:blog.stackademic.com/go-concurre...
原文作者:Brian NQC
译者:Regan Yue
P.S. 原文作者并没有审校过本译文,且译者在翻译本内容时夹带有个人对原文的理解,并尝试对其进行解读。可能理解或解读有误,麻烦请在评论区指出!
Go语言的并发编程是一个热门的话题。如何正确地使用锁机制来保证数据访问的线程安全一直是一个值得探讨的问题。
文章从一个多人看电视的例子入手,通过多个Gopher的行为说明了sync.RWMutex的Lock/Unlock和RLock/RUnlock的区别。Lock用于独占性写入,RLock允许同时读取。还介绍了RLocker接口的作用。并给出了代码示例。
sync.RWMutex是Go语言中处理并发访问的一个有用的工具。正确地理解和使用它,可以帮助我们编写出更加安全、高效的Go代码。
一、情景演示
假设房间里有 4 只 Gopher 和1台电视。每个人都可以一起看电视,但如果有人想换台,这个人就必须独占这台电视(也就是房间没人,并且这个人进房间前就想换台)。也就是说,这时其他人既不需要换台,也不能正在看电视。
一开始,电视是关着的。Partier 过来打开了电视。因为他需要改变电视的状态,所以他需要一个人在房间拿着电视遥控器。由于电视遥控器目前是可用的(未锁定),因此他可以立即使用电视机。
当 Partier 拿着电视遥控器时,Candier 想要换到另一个节目。她调用 tv.Lock(),要求独占电视遥控器。不过,她必须等到 Partier 放下电视遥控器,才能进房间换到她想看的电视。
这时 Stringer 也来了,他只想看当前的节目,于是调用了 tv.RLock()。由于 Candier 仍拿着电视遥控器,Stringer 需要在房间外等待。过了一会儿,Swimmer 来了。他也不想换台,观看当前的节目。因此,他和 Stringer 一起加入了队列。
Candier 放下了电视遥控器。Stringer 和 Swimmer 现在可以一起看电视了。后来,Partier 又回来了,这次他只想安静看电视,没有想调台或者关闭电视机。他与 Stringer 和 Swimmer 一同进房间看电视。
这时,Swimmer 说: 新闻太无聊了,他想换到其他频道。但是他目前持有的 RLock() 不允许他换台。为了进行换台操作,他需要先执行 tv.RUnlock(),然后再执行 tv.Lock()。但 Swimmer 和 Partier 仍在观看当前节目,电视遥控器仍处于 RLocked 状态,使得 Swimmer 必须先等待。
过了一会儿,Partier 离开了,但电视遥控器仍处于 RLocked 状态,因为 Stringer 还在那里看着呢。Candier 又回来了。这次她只想看当前的电视节目,这时尽管电视遥控器处于 RLocked 状态,但 Candier 仍需要排队。这与刚才 Partier 想要与 Stringer 和 Swimmer 一起观看当前的电视节目时不同,因为 Swimmer 在 Candier 回来之前就想换台,他在外面等着,不愿意去看那无聊的、他不想看的节目。 Swimmer 排在 Candier 前面,且与 Candier 想要的操作不同。
Stringer 放下电视遥控器,离开房间。由于无人观看,电视机遥控器变为 Unlocked 状态,允许 Swimmer 锁定电视机遥控器来更换频道。
在换台后,Swimmer放下了电视遥控器,离开房间,让 Candier 用 RLock 观看电视。如您所见,Candier 想看的电视是新闻节目,但现在是体育节目。她甚至不知道(也不在乎)电视节目的变化。她看了一会儿体育节目,然后放开了电视遥控器。在一大群人来来往往之后,电视机的最终状态是 Unlocked 并且在播放体育节目。
二、案例代码
使用sync.RWMutex相当简单。按照惯例,我们通常将sync.RWMutex和它所保护的变量放在一起或者放在同一个结构体中。
go
package main
import (
"log"
"sync"
"time"
)
type TV struct {
mu sync.RWMutex
show string
}
func main() {
tv := &TV{show: "Off"}
go changeTVChannel("Partier", "Drama", tv)
go changeTVChannel("Candier", "News", tv)
go watchTV("Stringer", tv)
go watchTV("Swimmer", tv)
go watchTV("Partier", tv)
go changeTVChannel("Swimmer", "Sport", tv)
go watchTV("Candier", tv)
time.Sleep(5 * time.Second) // For brevity, better use sync.WaitGroup
log.Println("Main: Done, shutting down")
}
func watchTV(name string, tv *TV) {
log.Printf("%v: I want to watch the TV\n", name)
tv.mu.RLock()
log.Printf("%v: I'm watching the TV, show: %v\n", name, tv.show)
time.Sleep(500 * time.Millisecond)
log.Printf("%v: I don't want to watch any more\n", name)
tv.mu.RUnlock()
}
func changeTVChannel(name string, show string, tv *TV) {
log.Printf("%v: I want to change the channel to %v\n", name, show)
tv.mu.Lock()
lastShow := tv.show
tv.show = show
time.Sleep(200 * time.Millisecond)
log.Printf("%v: Changed channel from %v to %v\n", name, lastShow, show)
tv.mu.Unlock()
}
RLocker()方法
sync.RWMutex 中有4个主要的方法:
- Lock()和Unlock()用于独占写入
- RLock()和RUnlock()用于读取
你可能会觉得Lock()/Unlock()方法的命名有点令人困惑。WLock()/WUnlock()不是更让人容易理解吗?答案很简单,类似于sync.Mutex,sync.RWMutex也是实现的sync.Locker 接口,并且这两个方法都在该接口中定义。甚至可以在所有使用情况中将sync.Mutex替换为sync.RWMutex。虽然它们的实现方式不同,性能可能有所不同,但从功能层次上来说,它们的作用是相同的。
go
type MyStruct struct {
mu sync.Locker
val int
}
// This works
myStruct := &MyStruct{mu: sync.Mutext{}}
// This also works and behaves the same
// Though, doing so is questionable
myStruct := &MyStruct{mu: sync.RWMutext{}}
sync.RWMutex还提供了RLocker()方法。当我们希望确保用于读取的那个goroutine永远不会错误地获取写入锁时,这个方法非常有用。
go
// RLocker returns a Locker interface that implements
// the Lock and Unlock methods by calling rw.RLock and rw.RUnlock.
func (rw *RWMutex) RLocker() Locker {
return (*rlocker)(rw)
}
type rlocker RWMutex
func (r *rlocker) Lock() { (*RWMutex)(r).RLock() }
func (r *rlocker) Unlock() { (*RWMutex)(r).RUnlock() }