zoukankan      html  css  js  c++  java
  • [转]Go 互斥锁(sync.Mutex)和 读写锁(sync.RWMutex)

    原文:https://www.cnblogs.com/kaichenkai/p/11108303.html

    _______________________________________________

    什么时候需要用到锁?

    当程序中就一个线程的时候,是不需要加锁的,但是通常实际的代码不会只是单线程,所以这个时候就需要用到锁了,那么关于锁的使用场景主要涉及到哪些呢?

    • 多个线程在读相同的数据时
    • 多个线程在写相同的数据时
    • 同一个资源,有读又有写

    互斥锁(sync.Mutex)

    互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个 goroutine 可以访问到共享资源(同一个时刻只有一个线程能够拿到锁

    先通过一个并发读写的例子演示一下,当多线程同时访问全局变量时,结果会怎样?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package main
    import ("fmt")
     
    var count int
     
    func main() {
        for i := 0; i < 2; i++ {
            go func() {
                for i := 1000000; i > 0; i-- {
                    count ++
                }
                fmt.Println(count)
            }()
        }
     
        fmt.Scanf(" ")  //等待子线程全部结束
    }
     
    运行结果:
    980117
    1011352  //最后的结果基本不可能是我们想看到的:200000

    修改代码,在累加的地方添加互斥锁,就能保证我们每次得到的结果都是想要的值

    复制代码
    package main
    import ("fmt"
        "sync"
    )
    
    var (
        count int
        lock sync.Mutex
    )
    
    func main() {
        for i := 0; i < 2; i++ {
            go func() {
                for i := 1000000; i > 0; i-- {
                    lock.Lock()
                    count ++
                    lock.Unlock()
                }
                fmt.Println(count)
            }()
        }
    
        fmt.Scanf("
    ")  //等待子线程全部结束
    }
    
    运行结果:
    1952533
    2000000  //最后的线程打印输出
    复制代码

    读写锁(sync.RWMutex)

    在读多写少的环境中,可以优先使用读写互斥锁(sync.RWMutex),它比互斥锁更加高效。sync 包中的 RWMutex 提供了读写互斥锁的封装

    读写锁分为:读锁和写锁

    • 如果设置了一个写锁,那么其它读的线程以及写的线程都拿不到锁,这个时候,与互斥锁的功能相同
    • 如果设置了一个读锁,那么其它写的线程是拿不到锁的,但是其它读的线程是可以拿到锁

    通过设置写锁,同样可以实现数据的一致性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package main
    import ("fmt"
        "sync"
    )
     
    var (
        count int
        rwLock sync.RWMutex
    )
     
    func main() {
        for i := 0; i < 2; i++ {
            go func() {
                for i := 1000000; i > 0; i-- {
                    rwLock.Lock()
                    count ++
                    rwLock.Unlock()
                }
                fmt.Println(count)
            }()
        }
     
        fmt.Scanf(" ")  //等待子线程全部结束
    }
     
    运行结果:
    1968637
    2000000

    互斥锁和读写锁的性能对比

    demo:制作一个读多写少的例子,分别开启 3 个 goroutine 进行读和写,输出最终的读写次数

    1)使用互斥锁:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    package main
    import (
        "fmt"
        "sync"
        "time"
    )
     
    var (
        count  int
        //互斥锁
        countGuard sync.Mutex
    )
     
    func read(mapA map[string]string){
        for {
            countGuard.Lock()
            var _ string = mapA["name"]
            count += 1
            countGuard.Unlock()
        }
    }
     
    func write(mapA map[string]string) {
        for {
            countGuard.Lock()
            mapA["name"] = "johny"
            count += 1
            time.Sleep(time.Millisecond * 3)
            countGuard.Unlock()
        }
    }
     
     
     
    func main() {
        var num int = 3
        var mapA map[string]string = map[string]string{"nema"""}
     
        for i := 0; i < num; i++ {
            go read(mapA)
        }
     
        for i := 0; i < num; i++ {
            go write(mapA)
        }
     
        time.Sleep(time.Second * 3)
        fmt.Printf("最终读写次数:%d ", count)
    }
     
    运行结果:
    最终读写次数:3766

    2)使用读写锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    package main
    import (
        "fmt"
        "sync"
        "time"
    )
     
    var (
        count  int
        //读写锁
        countGuard sync.RWMutex
    )
     
    func read(mapA map[string]string){
        for {
            countGuard.RLock()  //这里定义了一个读锁
            var _ string = mapA["name"]
            count += 1
            countGuard.RUnlock()
        }
    }
     
    func write(mapA map[string]string) {
        for {
            countGuard.Lock()  //这里定义了一个写锁
            mapA["name"] = "johny"
            count += 1
            time.Sleep(time.Millisecond * 3)
            countGuard.Unlock()
        }
    }
     
     
     
    func main() {
        var num int = 3
        var mapA map[string]string = map[string]string{"nema"""}
     
        for i := 0; i < num; i++ {
            go read(mapA)
        }
     
        for i := 0; i < num; i++ {
            go write(mapA)
        }
     
        time.Sleep(time.Second * 3)
        fmt.Printf("最终读写次数:%d ", count)
    }
     
    运行结果:
    最终读写次数:8165

    结果差距大概在 2 倍左右,读锁的效率要快很多!

    关于互斥锁的补充

    互斥锁需要注意的问题:

    1. 不要重复锁定互斥锁
    2. 不要忘记解锁互斥锁,必要时使用 defer 语句
    3. 不要在多个函数之间直接传递互斥锁

    死锁: 当前程序中的主 goroutine 以及我们启用的那些 goroutine 都已经被阻塞,这些 goroutine 可以被称为用户级的 goroutine 这就相当于整个程序已经停滞不前了,并且这个时候 go 程序会抛出如下的 panic:

    1
    fatal error: all goroutines are asleep - deadlock!

    并且go语言运行时系统抛出自行抛出的panic都属于致命性错误,都是无法被恢复的,调用recover函数对他们起不到任何作用

    Go语言中的互斥锁是开箱即用的,也就是我们声明一个sync.Mutex 类型的变量,就可以直接使用它了,需要注意:该类型是一个结构体类型,属于值类型的一种,将它当做参数传给一个函数,将它从函数中返回,把它赋值给其他变量,让它进入某个管道,都会导致他的副本的产生。并且原值和副本以及多个副本之间是完全独立的,他们都是不同的互斥锁,所以不应该将锁通过函数的参数进行传递

    关于读写锁的补充

    1、在写锁已被锁定的情况下再次试图锁定写锁,会阻塞当前的goroutine

    2、在写锁已被锁定的情况下再次试图锁定读锁,也会阻塞当前的goroutine

    3、在读锁已被锁定的情况下试图锁定写锁,同样会阻塞当前的goroutine

    4、在读锁已被锁定的情况下再试图锁定读锁,并不会阻塞当前的goroutine

    对于某个受到读写锁保护的共享资源,多个写操作不能同时进行,写操作和读操作也不能同时进行,但多个读操作却可以同时进行

    对写锁进行解锁,会唤醒“所有因试图锁定读锁,而被阻塞的goroutine”, 并且这个通常会使他们都成功完成对读锁的锁定(这个还不理解)

    对读锁进行解锁,只会在没有其他读锁锁定的前提下,唤醒“因试图锁定写锁,而被阻塞的 goroutine” 并且只会有一个被唤醒的 goroutine 能够成功完成对写锁的锁定,其他的 goroutine 还要在原处继续等待,至于哪一个goroutine,那么就要看谁等待的事件最长

    解锁读写锁中未被锁定的写锁, 会立即引发panic ,对其中的读锁也是如此,并且同样是不可恢复的

    参考链接:https://www.cnblogs.com/zhaof/p/8636384.html

    ending ~

    每天都要遇到更好的自己.
  • 相关阅读:
    asp.net图片上传代码
    C#(同步调用、异步调用、异步回调)
    跨线程的控件调用
    C# ListView用法详解 很完整
    获取dataGridView双击时判断双击的是下面的行,还是列头
    加密解密类
    C# 中DataGridView 绑定List<T>做数据源的操作问题
    C#获取当前程序集的完整路径
    接口测试-小结
    接口测试用例
  • 原文地址:https://www.cnblogs.com/oxspirt/p/13957166.html
Copyright © 2011-2022 走看看