8.9 Go语言sync包与锁 - Go语言中文社区

8.9 Go语言sync包与锁


Go语言中 sync 包里提供了互斥锁 Mutex 和读写锁 RWMutex 用于处理并发过程中可能出现同时两个或多个协程(或线程)读或写同一个变量的情况。

为什么需要锁

锁是 sync 包中的核心,它主要有两个方法,分别是加锁(Lock)和解锁(Unlock)。

在并发的情况下,多个线程或协程同时其修改一个变量,使用锁能保证在某一时间内,只有一个协程或线程修改这一变量。

不使用锁时,在并发的情况下可能无法得到想要的结果,如下所示:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func 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 的值依次递增输出,然而实际结果却是下面这样子的。

  • 537
  • 995
  • 996
  • 997
  • 538
  • 999
  • 1000

通过运行结果可以看出 a 的值并不是按顺序递增输出的,这是为什么呢?

协程的执行顺序大致如下所示:

  1. 从寄存器读取 a 的值;
  2. 然后做加法运算;
  3. 最后写到寄存器。

按照上面的顺序,假如有一个协程取得 a 的值为 3,然后执行加法运算,此时又有一个协程对 a 进行取值,得到的值同样是 3,最终两个协程的返回结果是相同的。

而锁的概念就是,当一个协程正在处理 a 时将 a 锁定,其它协程需要等待该协程处理完成并将 a 解锁后才能再进行操作,也就是说同时处理 a 的协程只能有一个,从而避免上面示例中的情况出现。

互斥锁 Mutex

上面的示例中出现的问题怎么解决呢?加一个互斥锁 Mutex 就可以了。那什么是互斥锁呢 ?互斥锁中其有两个方法可以调用,如下所示:

  1. func (m *Mutex) Lock()
  2. func (m *Mutex) Unlock()

将上面的代码略作修改,如下所示:

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. )
  7. func main() {
  8. var a = 0
  9. var lock sync.Mutex
  10. for i := 0; i < 1000; i++ {
  11. go func(idx int) {
  12. lock.Lock()
  13. defer lock.Unlock()
  14. a += 1
  15. fmt.Printf("goroutine %d, a=%d\n", idx, a)
  16. }(i)
  17. }
  18. // 等待 1s 结束主程序
  19. // 确保所有协程执行完
  20. time.Sleep(time.Second)
  21. }

运行结果如下:

  • goroutine 995, a=996
  • goroutine 996, a=997
  • goroutine 997, a=998
  • goroutine 998, a=999
  • goroutine 999, a=1000

需要注意的是一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定),示例代码如下:

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. )
  7. func 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("goroutine2: 等待解锁")
  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. }

上面的代码运行结果如下:

  • goroutine1: 我会锁定大概 2s
  • goroutine2: 等待解锁
  • goroutine1: 我解锁了,你们去抢吧
  • goroutine2: 欧耶,我也解锁了

读写锁

读写锁有如下四个方法:

  • 写操作的锁定和解锁分别是func (RWMutex) Lock和func (RWMutex) Unlock;
  • 读操作的锁定和解锁分别是func (RWMutex) Rlock和func (RWMutex) RUnlock。

读写锁的区别在于:

  • 当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;
  • 当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;
  • 当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。

所以说这里的读锁定(RLock)目的其实是告诉写锁定,有很多协程或者进程正在读取数据,写操作需要等它们读(读解锁)完才能进行写(写锁定)。

我们可以将其总结为如下三条:

  • 同时只能有一个 goroutine 能够获得写锁定;
  • 同时可以有任意多个 gorouinte 获得读锁定;
  • 同时只能存在写锁定或读锁定(读和写互斥)。

示例代码如下所示:

  1. package main
  2. import (
  3. "fmt"
  4. "math/rand"
  5. "sync"
  6. )
  7. var count int
  8. var rw sync.RWMutex
  9. func 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. }
  21. func 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. }
  29. func 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. }

其执行结果如下:

  • goroutine 0 进入读操作…
  • goroutine 0 读取结束,值为:0
  • goroutine 3 进入读操作…
  • goroutine 1 进入读操作…
  • goroutine 3 读取结束,值为:0
  • goroutine 1 读取结束,值为:0
  • goroutine 4 进入写操作…
  • goroutine 4 写入结束,新值为:81
  • goroutine 4 进入读操作…
  • goroutine 4 读取结束,值为:81
  • goroutine 2 进入读操作…
  • goroutine 2 读取结束,值为:81
  • goroutine 0 进入写操作…
  • goroutine 0 写入结束,新值为:887
  • goroutine 1 进入写操作…
  • goroutine 1 写入结束,新值为:847
  • goroutine 2 进入写操作…
  • goroutine 2 写入结束,新值为:59
  • goroutine 3 进入写操作…
  • goroutine 3 写入结束,新值为:81

下面再来看两个示例。

【示例 1】多个读操作同时读取一个变量时,虽然加了锁,但是读操作是不受影响的。(读和写是互斥的,读和读不互斥)

  1. package main
  2. import (
  3. "sync"
  4. "time"
  5. )
  6. var m *sync.RWMutex
  7. func main() {
  8. m = new(sync.RWMutex)
  9. // 多个同时读
  10. go read(1)
  11. go read(2)
  12. time.Sleep(2*time.Second)
  13. }
  14. func 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. }

运行结果如下:

  • 1 read start
  • 1 reading
  • 2 read start
  • 2 reading
  • 1 read over
  • 2 read over

【示例 2】由于读写互斥,所以写操作开始的时候,读操作必须要等写操作进行完才能继续,不然读操作只能继续等待。

  1. package main
  2. import (
  3. "sync"
  4. "time"
  5. )
  6. var m *sync.RWMutex
  7. func 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. }
  15. func 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. }
  23. func 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. }

运行结果如下:

  • 1 write start
  • 3 write start
  • 1 writing
  • 2 read start
  • 1 write over
  • 2 reading
版权声明:本教程内容除了本站原创内容外,还有来源自C语言编程网,博客园,CSDN等技术站点,感谢相关博主原创文章,转载请附上原文出处链接和本声明。
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 阅读 ( 612 )
  • 分类:Go

0 条评论

官方社群

GO教程

猜你喜欢