• GO 语言处理并发的时候我们是选择sync还是channel


    以前写 C 的时候,我们一般是都通过共享内存来通信,对于并发去操作某一块数据时,为了保证数据安全,控制线程间同步,我们们会去使用互斥锁,加锁解锁来进行处理

    然而 GO 语言中建议的时候通过通信来共享内存,使用 channel 来完成临界区的同步机制

    可是 GO 语言中的 channel 毕竟是属于比较高级的原语,自然在性能上就比不上 sync包里面的锁机制,感兴趣的同学可以自己写一个简单的基准测试来确认一下效果,评论去可以交流

    另外,使用 sync 包来控制同步时,我们不会失去结构对象的所有权,还能让多个协程之间同步访问临界区的资源,那么如果我们的需求能够符合这种情况时,还是建议使用 sync 包来控制同步更加的合理和高效

    为什么会选择使用 sync 包来控制同步结论:

    1. 不期望失去结构的控制权的同时,还期望多个协程能够安全的同步访问临界区资源
    2. 对性能要求会更高的情况

    sync 的 Mutex 和 RWMutex

    查看 sync 包的源码(xxx\Go\src\sync),我们可以看到 sync 包下面有如下几个结构:

    1. Mutex
    2. RWMutex
    3. Once
    4. Cond
    5. Pool
    6. atomic 包原子操作

    上述经常使用的就是 Mutex 了,尤其是最开始不善于使用 channel 的时候,觉得使用 Mutex 非常的顺手,其次 RWMutex 相对来说就会用的少一些

    不知大家有没有关注过,使用 Mutex 和 使用 RWMutex 的性能表现,获取大部分人都是默认使用互斥锁,一起写个 demo 来看看 他俩的性能对比

    var (
            mu   sync.Mutex
            murw sync.RWMutex
            tt1  = 1
            tt2  = 2
            tt3  = 3
    )
    
    // 使用 Mutex 控制读取数据
    func BenchmarkReadMutex(b *testing.B) {
            b.RunParallel(func(pp *testing.PB) {
                    for pp.Next() {
                            mu.Lock()
                            _ = tt1
                            mu.Unlock()
                    }
            })
    }
    
    // 使用 RWMutex 控制读取数据
    func BenchmarkReadRWMutex(b *testing.B) {
            b.RunParallel(func(pp *testing.PB) {
                    for pp.Next() {
                            murw.RLock()
                            _ = tt2
                            murw.RUnlock()
                    }
            })
    }
    
    // 使用 RWMutex 控制读写入数据
    func BenchmarkWriteRWMutex(b *testing.B) {
            b.RunParallel(func(pp *testing.PB) {
                    for pp.Next() {
                            murw.Lock()
                            tt3++
                            murw.Unlock()
                    }
            })
    }
    
    • 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

    写了三个简单的基准测试

    1. 使用互斥锁读取数据
    2. 使用读写锁的读锁读取数据
    3. 使用读写锁读取和写入数据
    $ go test -bench . bbb_test.go --cpu 2
    goos: windows
    goarch: amd64
    cpu: Intel(R) Core(TM)2 Duo CPU     T7700  @ 2.40GHz
    BenchmarkReadMutex-2            39638757                30.45 ns/op
    BenchmarkReadRWMutex-2          43082371                26.97 ns/op
    BenchmarkWriteRWMutex-2         16383997                71.35 ns/op
    
    
    $ go test -bench . bbb_test.go --cpu 4
    goos: windows
    goarch: amd64
    cpu: Intel(R) Core(TM)2 Duo CPU     T7700  @ 2.40GHz
    BenchmarkReadMutex-4            17066666                73.47 ns/op
    BenchmarkReadRWMutex-4          43885633                30.33 ns/op
    BenchmarkWriteRWMutex-4         10593098               110.3 ns/op
    
    
    $ go test -bench . bbb_test.go --cpu 8
    goos: windows
    goarch: amd64
    cpu: Intel(R) Core(TM)2 Duo CPU     T7700  @ 2.40GHz
    BenchmarkReadMutex-8             8969340               129.0 ns/op
    BenchmarkReadRWMutex-8          36451077                33.46 ns/op
    BenchmarkWriteRWMutex-8          7728303               158.5 ns/op
    
    
    
    $ go test -bench . bbb_test.go --cpu 16
    goos: windows
    goarch: amd64
    cpu: Intel(R) Core(TM)2 Duo CPU     T7700  @ 2.40GHz
    BenchmarkReadMutex-16            8533333               132.6 ns/op
    BenchmarkReadRWMutex-16         39638757                29.98 ns/op
    BenchmarkWriteRWMutex-16         6751646               173.9 ns/op
    
    
    
    $ go test -bench . bbb_test.go --cpu 128
    goos: windows
    goarch: amd64
    cpu: Intel(R) Core(TM)2 Duo CPU     T7700  @ 2.40GHz
    BenchmarkReadMutex-128          10155368               116.0 ns/op
    BenchmarkReadRWMutex-128        35108558                33.27 ns/op
    BenchmarkWriteRWMutex-128        6334021               195.3 ns/op
    
    • 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

    可以看出来当并发较小的时候,使用互斥锁和使用读写锁的读锁性能类似,当并发逐渐变大时,读写锁的读锁性能并未发生较大变化,互斥锁和读写锁的性能都会随着并发的变大而下降

    那么很明显,读写锁适用于读多写少的场景,在大并发读书数据的时候,多个协程可以同时拿到读锁,减少锁竞争和等待时间

    而互斥锁并发的时候,多个协程中,只有一个协程能拿到锁,其他协程就会阻塞和等待,影响性能

    举个例子,我们正常使用互斥锁,看看可能会出现什么样的问题

    使用 sync 需要注意的地方

    平时使用 sync 包中的锁的时候,需要注意的是不要去拷贝已经已经使用过的 Mutex 或者是 RWMutex

    写一个简单的 demo:

    var mu sync.Mutex
    
    // sync 的互斥锁,读写锁,在被使用之后,就不要去复制这个对象,若要复制,需要在其未被使用的时候
    func main() {
    
        go func(mm sync.Mutex) {
                for {
                        mm.Lock()
                        time.Sleep(time.Second * 1)
                        fmt.Println("g2")
                        mm.Unlock()
                }
        }(mu)
    
        mu.Lock()
        go func(mm sync.Mutex) {
                for {
                        mm.Lock()
                        time.Sleep(time.Second * 1)
                        fmt.Println("g3")
                        mm.Unlock()
                }
        }(mu)
    
        time.Sleep(time.Second * 1)
        fmt.Println("g1")
    
        mu.Unlock()
    
        time.Sleep(time.Second * 20)
    }
    
    • 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

    感兴趣的朋友的,可以运行一下,可以看到打印的结果中时没有 g3 的,因此 g3 所在的协程已经发生了死锁,没有机会去调用 unlock

    出现这种情况的原因是这样的,先来看看 Mutex 的内部结构:

    //...
    // A Mutex must not be copied after first use.
    //...
    type Mutex struct {
            state int32
            sema  uint32
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    因为例如 Mutex 中的内部结构是有一个 state (表示互斥锁的状态)和 sema(表示控制互斥锁的信号量),其中初始化 Mutex 的时候,他们都是 0,但是当我们用 Mutex 加锁时,Mutex 的状态就变成了 Locked 的状态,这个时候,其中一个协程去拷贝这个 Mutex,并在自己协程中加锁,就会出现死锁的情况,这一点是非常需要注意的

    如果涉及到这种多个协程使用 Mutex 的情况, 可以使用闭包或者传入包裹锁的结构地址或者指针,这样就可以避免使用锁的时候导致不可预期的结果,避免一脸蒙圈

    sync.Once

    sync 包中的其他成员,不知 xdm 使用的多么,相对使用频率较高的应该就是 sync.Once 了,其他成员 xdm 可以自行看看源码,或者评论区留言哦,我们来看看 syn.Once 如何使用,都有哪些需要注意的?

    还记得之前写 C 或者 C++ 的时候,对于程序生命周期只有一个实例的时候,我们会选择使用单例模式来进行处理,那么此处的 sync.Once 就是非常适合用在单例模式中

    sync.Once 可以保证任意一个函数在程序运行期间只被执行一次,这一点相对来说就比每个包中的 init 函数灵活一些了

    这里需要注意,sync.Once 中执行的函数,如果出现了 panic ,也是会被认为是执行完了了一次,之后如果再有逻辑需要进入 sync.Once 是无法进入并执行函数逻辑的

    一般情况下, sync.Once 用于对象资源的初始化和清理动作,避免重复操作,可以来看一个 demo:

    1. 主函数开辟 3 个协程,且使用 sync.WaitGroup 来管控并等待子协程退出
    2. 主函数开辟所有协程之后等待 2 秒,开始创建并获取实例
    3. 协程中也在获取实例
    4. 只要有一个协程获取到进入 Once,执行逻辑之后,会出现 panic
    5. 出现 panic 的协程捕获了异常,此时全局的 instance 已经被初始化,其他协程仍然无法进入 Once 内的函数
    type Instance struct {
            Name string
    }
    
    var instance *Instance
    var on sync.Once
    
    func GetInstance(num int) *Instance {
    
            defer func() {
                    if err := recover(); err != nil {
                            fmt.Println("num %d ,get instance and catch error ... \n", num)
                    }
            }()
    
            on.Do(func() {
                    instance = &Instance{Name: "阿兵云原生"}
                    fmt.Printf("%d enter once ... \n", num)
                    panic("panic....")
            })
    
            return instance
    }
    
    func main() {
    
            var wg sync.WaitGroup
            for i := 0; i < 3; i++ {
                    wg.Add(1)
                    go func(i int) {
                            ins := GetInstance(i)
                            fmt.Printf("%d: ins:%+v  , p=%p\n", i, ins, ins)
                            wg.Done()
                    }(i)
            }
    
            time.Sleep(time.Second * 2)
    
            ins := GetInstance(9)
            fmt.Printf("9: ins:%+v  , p=%p\n", ins, ins)
            wg.Wait()
    }
    
    • 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

    通过打印结果可以看出,0 对应的协程进入了 Once,且发生了 panic,因此当前协程获取到的 GetInstance 函数的结果是 nil

    其他的协程包括主协程调用 GetInstance 函数都能正常拿到 instance 的地址,可以看出地址是同一个,全局就只初始化了一次

    $ go run main.go
    0 enter once ...
    num %d ,get instance and catch error ...
     0
    0: ins:  , p=0x0
    1: ins:&{Name:阿兵云原生}  , p=0xc000086000
    2: ins:&{Name:阿兵云原生}  , p=0xc000086000
    9: ins:&{Name:阿兵云原生}  , p=0xc000086000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    总结

    1. 如何选择 sync 和 channel
    2. sync 锁的使用注意事项
    3. sync 互斥锁和读写锁的性能对比
    4. sync Once 的使用演示

    欢迎点赞,关注,收藏

    朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

    好了,本次就到这里

    技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

    我是阿兵云原生,欢迎点赞关注收藏,下次见~
    可以进入地址进行体验和学习:https://xxetb.xet.tech/s/3lucCI

  • 相关阅读:
    Spring Security 实现动态权限菜单方案(附源码)
    【回归预测-lssvm】基于粒子群算法优化最小二乘支持向量机lssvm实现数据回归预测附matlab代码
    MASA Auth - SSO与Identity设计
    节省工时超 1500人/天,国泰基金探索金融业人机协同新业态
    RK3399交叉编译问题
    Quectel EC200N-CN驱动移植记录
    外设驱动库开发笔记48:MCP4725单通道DAC驱动
    智慧农业数字孪生应用案例,数字化农业建设发展现状
    黑马Linux教程上新啦~是程序员都进来冲~
    基于瑞萨RZ/G2L存储读写速度与网络实测
  • 原文地址:https://blog.csdn.net/m0_37322399/article/details/133838152