golang sync包互斥锁和读写锁的使用
golang sync包里提供了 Locker接口、互斥锁 Mutex、读写锁 RWMutex用于处理并发过程中可能出现同时两个或多个协程(或线程)读或写同一个变量的情况。
一、为什么需要锁
在并发的情况下,多个线程或协程同时去修改一个变量。使用锁能保证在某一时间点内,只有一个协程或线程修改这一变量,具体我们可以看示例。先看不加锁的程序(会出现多个程序同时读该变量):
1package main
2import (
3 "fmt"
4 "time"
5)
6func main() {
7 var a = 0
8 for i := 0; i < 1000; i++ {
9 go func(idx int) {
10 a += 1
11 fmt.Println(a)
12 }(i)
13 }
14 time.Sleep(time.Second)
15}
从理论上来说,上面的函数是每次递增a的值的,所以理论上应该会有1000个不同的值输出,实际结果呢?
1[root@361way test]# go run l1.go |sort|uniq |wc -l
2998
3[root@361way test]# go run l1.go |sort|uniq |wc -l
41000
5[root@361way test]# go run l1.go |sort|uniq |wc -l
6998
7[root@361way test]# go run l1.go |sort|uniq |wc -l
8999
这里运行了4次,获取了三个不一样的结果。如果你有精力,可以将运行的结果逐一对比,在出现wc -l的结果小于1000时,绝对出现了重复值。为什么会现这样的情况?
协程依次执行:从寄存器读取 a 的值 -> 然后做加法运算 -> 最后写到寄存器。试想,此时一个协程取出 a 的值 3,正在做加法运算(还未写回寄存器)。同时另一个协程此时去取,取出了同样的 a 的值 3。最终导致的结果是,两个协程产出的结果相同,a 相当于只增加了 1。
所以,锁的概念就是,我正在处理 a(锁定),你们谁都别和我抢,等我处理完了(解锁),你们再处理。这样就实现了,同时处理 a 的协程只有一个,就实现了同步。
注:上面的方法是多协程的,增加runtime.GOMAXPROCS(4) 改为多进程多线程同样会有这样的问题。
二、互斥锁 Mutex
上面的示例中出现的问题怎么解决?加一个互斥锁 Mutex就OK了。哪什么是互斥锁 ?其有两个方法可以调用,如下:
1func (m *Mutex) Lock()
2func (m *Mutex) Unlock()
我们改下循环递增示例中的代码,如下:
1package main
2import (
3 "fmt"
4 "time"
5)
6func main() {
7 var a = 0
8 var lock sync.Mutex
9 for i := 0; i < 1000; i++ {
10 go func(idx int) {
11 lock.Lock()
12 defer lock.Unlock()
13 a += 1
14 fmt.Printf("goroutine %d, a=%d\n", idx, a)
15 }(i)
16 }
17 // 等待 1s 结束主程序
18 // 确保所有协程执行完
19 time.Sleep(time.Second)
20}
修改后执行的结果总是1000个不重服的值。而且使用go语言的lock锁一般不会出现忘了解锁的情况,因类其紧跟锁定的就是defer Unlock 。
需要注意的是一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定)。看如下代码:
1package main
2import (
3 "fmt"
4 "sync"
5 "time"
6)
7func main() {
8 ch := make(chan struct{}, 2)
9 var l sync.Mutex
10 go func() {
11 l.Lock()
12 defer l.Unlock()
13 fmt.Println("goroutine1: 我会锁定大概 2s")
14 time.Sleep(time.Second * 2)
15 fmt.Println("goroutine1: 我解锁了,你们去抢吧")
16 ch <- struct{}{}
17 }()
18 go func() {
19 fmt.Println("groutine2: 等待解锁")
20 l.Lock()
21 defer l.Unlock()
22 fmt.Println("goroutine2: 欧耶,我也解锁了")
23 ch <- struct{}{}
24 }()
25 // 等待 goroutine 执行结束
26 for i := 0; i < 2; i++ {
27 <-ch
28 }
29}
上面的代码执行结果如下:
1[root@361way test]# go run l2.go
2goroutine1: 我会锁定大概 2s
3groutine2: 等待解锁
4goroutine1: 我解锁了,你们去抢吧
5goroutine2: 欧耶,我也解锁了
三、读写锁
读写锁有如下四个方法:
1写操作的锁定和解锁
2* func (*RWMutex) Lock
3* func (*RWMutex) Unlock
4读操作的锁定和解锁
5* func (*RWMutex) Rlock
6* func (*RWMutex) RUnlock
注:区别在后的Lock和Unlock前有没有R 。
我们怎么理解读写锁呢?当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。所以说这里的读锁定(RLock)目的其实是告诉写锁定:有很多人正在读取数据,你给我站一边去,等它们读(读解锁)完你再来写(写锁定)。我们可以将其总结为如下三条:
- 同时只能有一个 goroutine 能够获得写锁定。
- 同时可以有任意多个 gorouinte 获得读锁定。
- 同时只能存在写锁定或读锁定(读和写互斥)。
看个示例:
1package main
2import (
3 "fmt"
4 "math/rand"
5 "sync"
6)
7var count int
8var rw sync.RWMutex
9func main() {
10 ch := make(chan struct{}, 10)
11 for i := 0; i < 5; i++ {
12 go read(i, ch)
13 }
14 for i := 0; i < 5; i++ {
15 go write(i, ch)
16 }
17 for i := 0; i < 10; i++ {
18 <-ch
19 }
20}
21func read(n int, ch chan struct{}) {
22 rw.RLock()
23 fmt.Printf("goroutine %d 进入读操作...\n", n)
24 v := count
25 fmt.Printf("goroutine %d 读取结束,值为:%d\n", n, v)
26 rw.RUnlock()
27 ch <- struct{}{}
28}
29func write(n int, ch chan struct{}) {
30 rw.Lock()
31 fmt.Printf("goroutine %d 进入写操作...\n", n)
32 v := rand.Intn(1000)
33 count = v
34 fmt.Printf("goroutine %d 写入结束,新值为:%d\n", n, v)
35 rw.Unlock()
36 ch <- struct{}{}
37}
其执行结果如下:
1[root@361way test]# go run l3.go
2goroutine 4 进入写操作...
3goroutine 4 写入结束,新值为:81
4goroutine 2 进入读操作...
5goroutine 2 读取结束,值为:81
6goroutine 3 进入读操作...
7goroutine 3 读取结束,值为:81
8goroutine 0 进入读操作...
9goroutine 0 读取结束,值为:81
10goroutine 1 进入读操作...
11goroutine 4 进入读操作...
12goroutine 4 读取结束,值为:81
13goroutine 1 读取结束,值为:81
14goroutine 0 进入写操作...
15goroutine 0 写入结束,新值为:887
16goroutine 1 进入写操作...
17goroutine 1 写入结束,新值为:847
18goroutine 3 进入写操作...
19goroutine 3 写入结束,新值为:59
20goroutine 2 进入写操作...
21goroutine 2 写入结束,新值为:81
再来看两个例子:
1package main
2import (
3 "sync"
4 "time"
5)
6var m *sync.RWMutex
7func main() {
8 m = new(sync.RWMutex)
9 // 多个同时读
10 go read(1)
11 go read(2)
12 time.Sleep(2*time.Second)
13}
14func read(i int) {
15 println(i,"read start")
16 m.RLock()
17 println(i,"reading")
18 time.Sleep(1*time.Second)
19 m.RUnlock()
20 println(i,"read over")
21}
这里例子中,多个读操作同时读一个操作。虽然加了锁,但都是读是不受影响的。(读和写是互斥的,读和读不互斥—-我靠,这是同性才是真爱的节奏啊)。
1package main
2import (
3 "sync"
4 "time"
5)
6var m *sync.RWMutex
7func main() {
8 m = new(sync.RWMutex)
9 // 写的时候啥也不能干
10 go write(1)
11 go read(2)
12 go write(3)
13 time.Sleep(2*time.Second)
14}
15func read(i int) {
16 println(i,"read start")
17 m.RLock()
18 println(i,"reading")
19 time.Sleep(1*time.Second)
20 m.RUnlock()
21 println(i,"read over")
22}
23func write(i int) {
24 println(i,"write start")
25 m.Lock()
26 println(i,"writing")
27 time.Sleep(1*time.Second)
28 m.Unlock()
29 println(i,"write over")
30}
由于读写互斥,上面这个示例中,写开始的时候,读必须要等写进行完才能继续。不然他只能继续等待,这就像只有一个茅坑,别我蹲着,你着急也不能去抢(为什么?有门关着呢!)。
捐赠本站(Donate)
如您感觉文章有用,可扫码捐赠本站!(If the article useful, you can scan the QR code to donate))
- Author: shisekong
- Link: https://blog.361way.com/rwmutex/5984.html
- License: This work is under a 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. Kindly fulfill the requirements of the aforementioned License when adapting or creating a derivative of this work.