读和写同时进行
虽然我们同一时间存款问题通过互斥锁得到了解决。但是如果同时存款与查询余额呢?
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.Add(n) for i := 1; i <= n; i++ { go func() { _ = user.Balance() wg.Done() }() } wg.Wait() fmt.Printf("%s's account banlance has %.2f $.", user.name, user.Balance()) }
然后我们运行代码,就又出现了线程竞争的问题:
$ go run -race main.go ================== WARNING: DATA RACE Read at 0x00c0000ba010 by goroutine 73: main.(*Account).Balance() /home/wade/GoProjects/Go RWMutex/v2/main.go:22 +0x44 main.main.func2() /home/wade/GoProjects/Go RWMutex/v2/main.go:43 +0x32 Previous write at 0x00c0000ba010 by goroutine 72: main.(*Account).Deposit() /home/wade/GoProjects/Go RWMutex/v2/main.go:17 +0x84 main.main.func1() /home/wade/GoProjects/Go RWMutex/v2/main.go:35 +0x46 Goroutine 73 (running) created at: main.main() /home/wade/GoProjects/Go RWMutex/v2/main.go:42 +0x1ba Goroutine 72 (finished) created at: main.main() /home/wade/GoProjects/Go RWMutex/v2/main.go:34 +0x15e ================== panic: sync: negative WaitGroup counter goroutine 2018 [running]: sync.(*WaitGroup).Add(0xc0000b4010, 0xffffffffffffffff) /usr/local/go/src/sync/waitgroup.go:74 +0x2e5 sync.(*WaitGroup).Done(...) /usr/local/go/src/sync/waitgroup.go:99 main.main.func2(0xc0000ba000, 0xc0000b4010) /home/wade/GoProjects/Go RWMutex/v2/main.go:44 +0x5d created by main.main /home/wade/GoProjects/Go RWMutex/v2/main.go:42 +0x1bb exit status 2
同理,我们需要对查询余额作同样的加锁处理:
func (a *Account) Balance() (balance float64) { a.mux.Lock() balance = a.balance a.mux.Unlock() return balance }
如果发生读写阻塞呢?我们利用 time.Sleep()
来模拟线程阻塞的过程:
package main import ( "log" "sync" "time" ) type Account struct { balance float64 mux sync.Mutex } // func (a *Account) Deposit(amount float64) { a.mux.Lock() // lock time.Sleep(time.Second * 2) a.balance += amount a.mux.Unlock() // unlock } func (a *Account) Balance() (balance float64) { a.mux.Lock() time.Sleep(time.Second * 2) balance = a.balance a.mux.Unlock() return balance } func main() { wg := &sync.WaitGroup{} user := &Account{} n := 5 wg.Add(n) for i := 1; i <= n; i++ { go func() { user.Deposit(1000) log.Printf("写:存款: %v", 1000) wg.Done() }() } wg.Add(n) for i := 1; i <= n; i++ { go func() { log.Printf("读:余额: %v", user.Balance()) wg.Done() }() } wg.Wait() }
我们在程序中,每隔两秒处理一次存款和查询操作,总共发生 5 次存款和 5 次查询,那么就需要 20 秒来执行这个程序。如果存款可以接受 2 秒的时间,但是读取应该只需要更快才对,即查询操作不应该发生阻塞。
$ go run -race main.go 2022/02/28 14:31:43 写:存款: 1000 2022/02/28 14:31:45 写:存款: 1000 2022/02/28 14:31:47 写:存款: 1000 2022/02/28 14:31:49 写:存款: 1000 2022/02/28 14:31:51 写:存款: 1000 2022/02/28 14:31:53 读:余额: 5000 2022/02/28 14:31:55 读:余额: 5000 2022/02/28 14:31:57 读:余额: 5000 2022/02/28 14:31:59 读:余额: 5000 2022/02/28 14:32:01 读:余额: 5000
读写锁:sync.RWMutex
Mutex 将所有的 goroutine 视为平等的,并且只允许一个 goroutine 获取锁。针对这种情况,读写锁就该被派上用场了。
RWMutex 是 Go 语言中内置的一个 reader/writer 锁,用来解决读者-写者问题(Readers–writers problem)。任意数量的读取器可以同时获取锁,或者单个写入器可以获取锁。 这个想法是读者只关心与写者的冲突,并且可以毫无困难地与其他读者并发执行。
Go 的读写锁的特点:多读单写。 RWMutex 结构更灵活,支持两类 goroutine:readers 和 writers。 在任意一时刻,一个 RWMutex 只能由任意数量的 readers 持有,或者只能由一个 writers 持有。
读写锁的四个方法
- RLock():此方法尝试获取读锁,并会阻塞直到被获取
- RUnlock():解锁读锁
- Lock():获取写锁,阻塞直到被获取
- UnLock():释放写锁
- RLocker():该方法返回一个指向 Locker 的指针,用于获取和释放读锁
读写锁演示
把互斥锁改为读写锁也很简单,只需要把 sync.Mutex
换成 sync.RWMutex
,然后在读操作的地方改为 RLock()
,释放读锁改为 RUnlock()
:
package main import ( "log" "sync" "time" ) type Account struct { balance float64 mux sync.RWMutex // 读写锁 } // func (a *Account) Deposit(amount float64) { a.mux.Lock() // 写锁 time.Sleep(time.Second * 2) a.balance += amount a.mux.Unlock() // 释放写锁 } func (a *Account) Balance() (balance float64) { a.mux.RLock() // 读锁 time.Sleep(time.Second * 2) balance = a.balance a.mux.RUnlock() // 释放读锁 return balance } func main() { wg := &sync.WaitGroup{} user := &Account{} n := 5 wg.Add(n) for i := 1; i <= n; i++ { go func() { user.Deposit(1000) log.Printf("写:存款: %v", 1000) wg.Done() }() } wg.Add(n) for i := 1; i <= n; i++ { go func() { log.Printf("读:余额: %v", user.Balance()) wg.Done() }() } wg.Wait() }
明显能感觉到读操作变快了,发生一次写之后,直接发生 6 次读操作,说明读操作是同时进行的,存款 1000 一次后,6 次读操作都是 1000 元,说明结果是正确的。
2022/02/28 14:42:50 写:存款: 1000 2022/02/28 14:42:52 读:余额: 1000 2022/02/28 14:42:52 读:余额: 1000 2022/02/28 14:42:52 读:余额: 1000 2022/02/28 14:42:52 读:余额: 1000 2022/02/28 14:42:52 读:余额: 1000 2022/02/28 14:42:52 读:余额: 1000 2022/02/28 14:42:54 写:存款: 1000 2022/02/28 14:42:56 写:存款: 1000 2022/02/28 14:42:58 写:存款: 1000
总结
本文从读者-写者问题出发,回顾了互斥锁的案例:一个银行账户存款和查询的竞争问题的出现以及解决方法。最后引出 Go 自带的读写锁 sync.RWMutex
。
读写锁的特点是多读单写,一个 RWMutex 只能由任意数量的 readers 持有,或者只能由一个 writers 持有。我们可以利用读写锁来锁定某个操作以防止其他例程/线程在处理它时更改值,防止程序出现不可预测的错误。最后,可以利用读写锁弥补互斥锁的缺陷,用来加快程序的读操作,减少程序的运行时间。