前言
这一篇文章我们来介绍 Go 语言帮我们实现的标准库的 sync.RWMutex{}
读写锁。
通过使用 sync.RWMutex
,我们的程序变得更加高效。
什么是读者-写者问题
先来了解读者-写者问题(Readers–writers problem)的背景。最基本的读者-写者问题首先由 Courtois 等人提出并解决。
读者-写者问题描述了计算机并发处理读写数据遇到的问题,如何保证数据完整性、一致性。解决读者-写者问题需保证对于一份资源操作满足以下下条件:
- 读写互斥
- 写写互斥
- 允许多个读者同时读取
解决读者-写者问题,可以采用读者优先(readers-preference)
方案或者写者优先(writers-preference)
方案。
- 读者优先(readers-preference):读者优先是读操作优先于写操作,即使写操作提出申请资源,但只要还有读者在读取操作,就还允许其他读者继续读取操作,直到所有读者结束读取,才开始写。读优先可以提供很高的并发处理性能,但是在频繁读取的系统中,会长时间写阻塞,导致写饥饿。
- 写者优先(writers-preference):写者优先是写操作优先于读操作,如果有写者提出申请资源,在申请之前已经开始读取操作的可以继续执行读取,但是如果再有读者申请读取操作,则不能够读取,只有在所有的写者写完之后才可以读取。写者优先解决了读者优先造成写饥饿的问题。但是若在频繁写入的系统中,会长时间读阻塞,导致读饥饿。
RWMutex 设计采用写者优先方法,保证写操作优先处理。
回顾一下互斥锁的案例
多次单笔存款
假设你有一个银行账户,那你既可以进行存钱,也可以查询余额的操作。
package main import "fmt" type Account struct { name string balance float64 } // func (a *Account) Deposit(amount float64) { a.balance += amount } func (a *Account) Balance() float64 { return a.balance } func main() { user := &Account{"xiaoW", 0} user.Deposit(10000) user.Deposit(200) user.Deposit(2022) fmt.Printf("%s's account balance has %.2f $.", user.name, user.Balance()) }
执行该代码,进行三笔存款,我们可以看到输出的账户余额为 12222.00 $:
$ go run main.go xiaoW's account balance has 12222.00 $.
同时多次存款
但如果我们进行同时存款呢?即使用 goroutine 来生成三个线程来模拟同时存款的操作。然后利用sync.WaitGroup
去等待所有 goroutine 执行完毕,打印最后的余额:
package main import ( "fmt" "sync" ) type Account struct { name string balance float64 } func (a *Account) Deposit(amount float64) { a.balance += amount } func (a *Account) Balance() float64 { return a.balance } func main() { var wg sync.WaitGroup user := &Account{"xiaoW", 0} wg.Add(3) go func() { user.Deposit(10000) wg.Done() }() go func() { user.Deposit(200) wg.Done() }() go func() { user.Deposit(2022) wg.Done() }() wg.Wait() fmt.Printf("%s's account banlance has %.2f $.", user.name, user.Balance()) }
同时执行 3 次是没问题的,但如果执行 1000 次呢?
package main import ( "fmt" "sync" ) type Account struct { name string balance float64 } // func (a *Account) Deposit(amount float64) { a.balance += amount } func (a *Account) Balance() float64 { return a.balance } func main() { var wg sync.WaitGroup user := &Account{"xiaoW", 0} n := 1000 wg.Add(n) for i := 1; i <= n; i++ { go func() { user.Deposit(1000) wg.Done() }() } wg.Wait() fmt.Printf("%s's account banlance has %.2f $.", user.name, user.Balance()) }
我们多次运行该程序,发现每次运行结果都不一样。
$ go run main.go xiaoW's account banlance has 0.00 $. $ go run main.go xiaoW's account banlance has 886000.00 $. $ go run main.go xiaoW's account banlance has 2000.00 $.
正常的结果应该为 1000 * 1000 = 1000000.00
的余额才对,运行很多次的情况下才能看到一次正常的结果。
xiaoW's account banlance has 1000000.00 $.
使用 -race 参数来查看数据竞争
我们可以利用 -race 参数来查看我们的代码是否有竞争:
$ go run -race main.go ================== WARNING: DATA RACE Read at 0x00c00000e040 by goroutine 7: main.(*Account).Deposit() /home/wade/GoProjects/Go RWMutex/v2/main.go:15 +0x48 main.main.func1() /home/wade/GoProjects/Go RWMutex/v2/main.go:31 +0x36 Previous write at 0x00c00000e040 by goroutine 45: main.(*Account).Deposit() /home/wade/GoProjects/Go RWMutex/v2/main.go:15 +0x6e main.main.func1() /home/wade/GoProjects/Go RWMutex/v2/main.go:31 +0x36 Goroutine 7 (running) created at: main.main() /home/wade/GoProjects/Go RWMutex/v2/main.go:30 +0x144 Goroutine 45 (finished) created at: main.main() /home/wade/GoProjects/Go RWMutex/v2/main.go:30 +0x144 ================== xiaoW's account banlance has 996000.00 $.Found 1 data race(s) exit status 66
我们可以看到了发生了 goroutine 的线程竞争,goroutine 7 在读的时候,goroutine 45 在写,最终导致了读写不一致,所以最终的余额也都不符合我们的预期。
互斥锁:sync.Mutex
对于上述发生的线程竞争问题,我们就可以使用互斥锁来解决,即同一时间只能有一个 goroutine 能够处理该函数。代码改正如下:
package main import ( "fmt" "sync" ) type Account struct { name string balance float64 mux sync.Mutex } // func (a *Account) Deposit(amount float64) { a.mux.Lock() // lock a.balance += amount a.mux.Unlock() // unlock } func (a *Account) Balance() float64 { return a.balance } func main() { var wg sync.WaitGroup user := &Account{} user.name = "xiaoW" n := 1000 wg.Add(n) for i := 1; i <= n; i++ { go func() { user.Deposit(1000) wg.Done() }() } wg.Wait() fmt.Printf("%s's account banlance has %.2f $.", user.name, user.Balance()) }
此时,我们再运行 3 次 go run -race main.go
,得到统一的结果:
$ go run -race main.go xiaoW's account banlance has 1000000.00 $. $ go run -race main.go xiaoW's account banlance has 1000000.00 $. $ go run -race main.go xiaoW's account banlance has 1000000.00 $.