zoukankan      html  css  js  c++  java
  • 【转】golang sync包互斥锁和读写锁的使用

    原文:http://www.361way.com/rwmutex/5984.html

    协程依次执行:从寄存器读取 a 的值 -> 然后做加法运算 -> 最后写到寄存器。试想,此时一个协程取出 a 的值 3,正在做加法运算(还未写回寄存器)。同时另一个协程此时去取,取出了同样的

    a 的值 3。最终导致的结果是,两个协程产出的结果相同,a 相当于只增加了 1。

    所以,锁的概念就是,我正在处理 a(锁定),你们谁都别和我抢,等我处理完了(解锁),你们再处理。这样就实现了,同时处理 a 的协程只有一个,就实现了同步。

    注:上面的方法是多协程的,增加runtime.GOMAXPROCS(4) 改为多进程多线程同样会有这样的问题

    ——————————————————————————————————————————————————

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

    一、为什么需要锁

    在并发的情况下,多个线程或协程同时去修改一个变量。使用锁能保证在某一时间点内,只有一个协程或线程修改这一变量,具体我们可以看示例。先看不加锁的程序(会出现多个程序同时读该变量):

    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的值的,所以理论上应该会有1000个不同的值输出,实际结果呢?

    1. [root@361way test]# go run l1.go |sort|uniq |wc -l
    2. 998
    3. [root@361way test]# go run l1.go |sort|uniq |wc -l
    4. 1000
    5. [root@361way test]# go run l1.go |sort|uniq |wc -l
    6. 998
    7. [root@361way test]# go run l1.go |sort|uniq |wc -l
    8. 999

    这里运行了4次,获取了三个不一样的结果。如果你有精力,可以将运行的结果逐一对比,在出现wc -l的结果小于1000时,绝对出现了重复值。为什么会现这样的情况?

    协程依次执行:从寄存器读取 a 的值 -> 然后做加法运算 -> 最后写到寄存器。试想,此时一个协程取出 a 的值 3,正在做加法运算(还未写回寄存器)。同时另一个协程此时去取,取出了同样的 a 的值 3。最终导致的结果是,两个协程产出的结果相同,a 相当于只增加了 1。

    所以,锁的概念就是,我正在处理 a(锁定),你们谁都别和我抢,等我处理完了(解锁),你们再处理。这样就实现了,同时处理 a 的协程只有一个,就实现了同步。

    注:上面的方法是多协程的,增加runtime.GOMAXPROCS(4) 改为多进程多线程同样会有这样的问题

    二、互斥锁 Mutex

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

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

    我们改下循环递增示例中的代码,如下:

    1. package main
    2. import (
    3. "fmt"
    4. "time"
    5. )
    6. func 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 ", idx, a)
    15. }(i)
    16. }
    17. // 等待 1s 结束主程序
    18. // 确保所有协程执行完
    19. time.Sleep(time.Second)
    20. }

    修改后执行的结果总是1000个不重服的值。而且使用go语言的lock锁一般不会出现忘了解锁的情况,因类其紧跟锁定的就是defer Unlock 。

    需要注意的是一个互斥锁只能同时被一个 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("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
    2. goroutine1: 我会锁定大概 2s
    3. groutine2: 等待解锁
    4. goroutine1: 我解锁了,你们去抢吧
    5. goroutine2: 欧耶,我也解锁了

    三、读写锁

    读写锁有如下四个方法:

    1. 写操作的锁定和解锁
    2. * func (*RWMutex) Lock
    3. * func (*RWMutex) Unlock
    4. 读操作的锁定和解锁
    5. * func (*RWMutex) Rlock
    6. * func (*RWMutex) RUnlock

    注:区别在后的Lock和Unlock前有没有R 。

    我们怎么理解读写锁呢?当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。所以说这里的读锁定(RLock)目的其实是告诉写锁定:有很多人正在读取数据,你给我站一边去,等它们读(读解锁)完你再来写(写锁定)。我们可以将其总结为如下三条:

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

    看个示例:

    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)
    24. v := count
    25. fmt.Printf("goroutine %d 读取结束,值为:%d ", 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)
    32. v := rand.Intn(1000)
    33. count = v
    34. fmt.Printf("goroutine %d 写入结束,新值为:%d ", n, v)
    35. rw.Unlock()
    36. ch <- struct{}{}
    37. }

    其执行结果如下:

    1. [root@361way test]# go run l3.go
    2. goroutine 4 进入写操作...
    3. goroutine 4 写入结束,新值为:81
    4. goroutine 2 进入读操作...
    5. goroutine 2 读取结束,值为:81
    6. goroutine 3 进入读操作...
    7. goroutine 3 读取结束,值为:81
    8. goroutine 0 进入读操作...
    9. goroutine 0 读取结束,值为:81
    10. goroutine 1 进入读操作...
    11. goroutine 4 进入读操作...
    12. goroutine 4 读取结束,值为:81
    13. goroutine 1 读取结束,值为:81
    14. goroutine 0 进入写操作...
    15. goroutine 0 写入结束,新值为:887
    16. goroutine 1 进入写操作...
    17. goroutine 1 写入结束,新值为:847
    18. goroutine 3 进入写操作...
    19. goroutine 3 写入结束,新值为:59
    20. goroutine 2 进入写操作...
    21. goroutine 2 写入结束,新值为:81

    再来看两个例子:

    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. 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. }

    由于读写互斥,上面这个示例中,写开始的时候,读必须要等写进行完才能继续。不然他只能继续等待,这就像只有一个茅坑,别我蹲着,你着急也不能去抢(为什么?有门关着呢!)。

  • 相关阅读:
    vue-router query和params参数的区别
    vue打包成app后,背景图片不显示
    vue打包成app后,点击手机上的物理返回按钮后直接退出app
    Echarts dataZoom 区域缩放
    CSS3 实现别样图型
    Echarts 不能百分比显示或显示有问题
    循环(数组循环、获取json数据循环)、each()循环详解
    页面数据加载完成时,显示loading页面.数据加载完,loading隐藏.
    获取可视区域高度赋值给div(解决document.body.clientHeight的返回值为0的问题)
    移动端rem布局,用户调整手机字体大小或浏览器字体大小后导致页面布局出错问题
  • 原文地址:https://www.cnblogs.com/oxspirt/p/14023890.html
Copyright © 2011-2022 走看看