脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - 如何使用 atomic 包减少锁冲突

如何使用 atomic 包减少锁冲突

2021-12-27 23:51Go语言中文网double12gzh Golang

Go 提供了 channel 或 mutex 等内存同步机制,有助于解决不同的问题。在共享内存的情况下,mutex 可以保护内存不发生数据竞争(data race)。不过,虽然存在两个 mutex,但 Go 也通过 atomic 包提供了原子内存基元来提高性能。在深入研究解

如何使用 atomic 包减少锁冲突

写在前面

本文基于 Golang 1.14

Go 提供了 channel 或 mutex 等内存同步机制,有助于解决不同的问题。在共享内存的情况下,mutex 可以保护内存不发生数据竞争(data race)。不过,虽然存在两个 mutex,但 Go 也通过 atomic 包提供了原子内存基元来提高性能。在深入研究解决方案之前,我们先回过头来看看数据竞争。

数据竞争

当两个或两个以上的 goroutine 同时访问同一块内存区域,并且其中至少有一个在写时,就会发生数据竞争。虽然 map 内部有一定的机制来防止数据竞争,但一个简单的结构体并没有任何的机制,因此容易发生数据竞争。

为了说明数据竞争,我以一个goroutine 持续更新的配置为例向大家展示一下。

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "sync"
  6. )
  7.  
  8. type Config struct {
  9. a []int
  10. }
  11.  
  12. func main() {
  13. cfg := &Config{}
  14.  
  15. // 启动一个 writer goroutine,不断写入数据
  16. go func() {
  17. i := 0
  18.  
  19. for {
  20. i++
  21. cfg.a = []int{i, i + 1, i + 2, i + 3, i + 4, i + 5}
  22. }
  23. }()
  24.  
  25. // 启动多个 reader goroutine,不断获取数据
  26. var wg sync.WaitGroup
  27. for n := 0; n < 4; n++ {
  28. wg.Add(1)
  29. go func() {
  30. for n := 0; n < 100; n++ {
  31. fmt.Printf("%#v\n", cfg)
  32. }
  33. wg.Done()
  34. }()
  35. }
  36.  
  37. wg.Wait()
  38. }

运行这段代码可以清楚地看到,原本期望是运行上述代码后,每一行的数字应该是连续的,但是由于数据竞争的存在,导致结果是非确定性的。

  1. F:\hello>go run main.go
  2. [...]
  3. &main.Config{a:[]int{180954, 180962, 180967, 180972, 180977, 180983}}
  4. &main.Config{a:[]int{181296, 181304, 181311, 181318, 181322, 181323}}
  5. &main.Config{a:[]int{181607, 181617, 181624, 181631, 181636, 181643}}

我们可以在运行时加入参数 --race 看一下结果:

  1. F:\hello>go run --race main.go
  2. [...]
  3. &main.Config{a:[]int(nil)}
  4. ==================
  5. &main.Config{a:[]int(nil)}
  6. WARNING: DATA RACE&main.Config{a:[]int(nil)}
  7.  
  8. Read at 0x00c00000c210 by Goroutine 9:
  9. reflect.Value.Int()
  10. D:/Go/src/reflect/value.go:988 +0x3584
  11. fmt.(*pp).printValue()
  12. D:/Go/src/fmt/print.go:749 +0x3590
  13. fmt.(*pp).printValue()
  14. D:/Go/src/fmt/print.go:860 +0x8f2
  15. fmt.(*pp).printValue()
  16. D:/Go/src/fmt/print.go:810 +0x289a
  17. fmt.(*pp).printValue()
  18. D:/Go/src/fmt/print.go:880 +0x261c
  19. fmt.(*pp).printArg()
  20. D:/Go/src/fmt/print.go:716 +0x26b
  21. fmt.(*pp).doPrintf()
  22. D:/Go/src/fmt/print.go:1030 +0x326
  23. fmt.Fprintf()
  24. D:/Go/src/fmt/print.go:204 +0x86
  25. fmt.Printf()
  26. D:/Go/src/fmt/print.go:213 +0xbc
  27. main.main.func2()
  28. F:/hello/main.go:31 +0x42
  29.  
  30. Previous write at 0x00c00000c210 by goroutine 7:
  31. main.main.func1()
  32. F:/hello/main.go:21 +0x66
  33.  
  34. goroutine 9 (running) created at:
  35. main.main()
  36. F:/hello/main.go:29 +0x124
  37.  
  38. goroutine 7 (running) created at:
  39. main.main()
  40. F:/hello/main.go:16 +0x95
  41. ==================

为了避免同时读写过程中产生的数据竞争最常采用的方法可能是使用 mutex 或 atomic 包。

Mutex?还是 Atomic?

标准库在 sync 包提供了两种互斥锁 :sync.Mutex 和 sync.RWMutex。后者在你的程序需要处理多个读操作和极少的写操作时进行了优化。

针对上面代码中产生的数据竞争问题,我们看一下,如何解决呢?

使用 sync.Mutex 解决数据竞争

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "sync"
  6. )
  7.  
  8. // Config 定义一个结构体用于模拟存放配置数据
  9. type Config struct {
  10. a []int
  11. }
  12.  
  13. func main() {
  14. cfg := &Config{}
  15. var mux sync.RWMutex
  16.  
  17. // 启动一个 writer goroutine,不断写入数据
  18. go func() {
  19. i := 0
  20.  
  21. for {
  22. i++
  23. // 进行数据写入时,先通过锁进行锁定
  24. mux.Lock()
  25. cfg.a = []int{i, i + 1, i + 2, i + 3, i + 4, i + 5}
  26. mux.Unlock()
  27. }
  28. }()
  29.  
  30. // 启动多个 reader goroutine,不断获取数据
  31. var wg sync.WaitGroup
  32. for n := 0; n < 4; n++ {
  33. wg.Add(1)
  34. go func() {
  35. for n := 0; n < 100; n++ {
  36. // 因为这里只是需要读取数据,所以只需要加一个读锁即可
  37. mux.RLock()
  38. fmt.Printf("%#v\n", cfg)
  39. mux.RUnlock()
  40. }
  41. wg.Done()
  42. }()
  43. }
  44.  
  45. wg.Wait()
  46. }

通过上面的代码,我们做了两处改动。第一处改动在写数据前通过 mux.Lock() 加了一把锁;第二处改动在读数据前通过 mux.RLock() 加了一把读锁。

运行上述代码看一下结果:

  1. F:\hello>go run --race main.go
  2. &main.Config{a:[]int{512, 513, 514, 515, 516, 517}}
  3. &main.Config{a:[]int{512, 513, 514, 515, 516, 517}}
  4. &main.Config{a:[]int{513, 514, 515, 516, 517, 518}}
  5. &main.Config{a:[]int{513, 514, 515, 516, 517, 518}}
  6. &main.Config{a:[]int{513, 514, 515, 516, 517, 518}}
  7. &main.Config{a:[]int{513, 514, 515, 516, 517, 518}}
  8. &main.Config{a:[]int{514, 515, 516, 517, 518, 519}}
  9. [...]

这次达到了我们的预期并且也没有产生数据竞争。

使用 atomic 解决数据竞争

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "sync"
  6. "sync/atomic"
  7. )
  8.  
  9. type Config struct {
  10. a []int
  11. }
  12.  
  13. func main() {
  14. var v atomic.Value
  15.  
  16. // 写入数据
  17. go func() {
  18. var i int
  19. for {
  20. i++
  21. cfg := Config{
  22. a: []int{i, i + 1, i + 2, i + 3, i + 4, i + 5},
  23. }
  24. v.Store(cfg)
  25. }
  26. }()
  27.  
  28. // 读取数据
  29. var wg sync.WaitGroup
  30. for n := 0; n < 4; n++ {
  31. wg.Add(1)
  32. go func() {
  33. for n := 0; n < 100; n++ {
  34. cfg := v.Load()
  35. fmt.Printf("%#v\n", cfg)
  36. }
  37. wg.Done()
  38. }()
  39. }
  40.  
  41. wg.Wait()
  42. }

这里我们使用了 atomic 包,通过运行我们发现,也同样达到了我们期望的结果:

  1. [...]
  2. main.Config{a:[]int{219142, 219143, 219144, 219145, 219146, 219147}}
  3. main.Config{a:[]int{219491, 219492, 219493, 219494, 219495, 219496}}
  4. main.Config{a:[]int{219826, 219827, 219828, 219829, 219830, 219831}}
  5. main.Config{a:[]int{219948, 219949, 219950, 219951, 219952, 219953}}

从生成的输出结果而言,看起来使用 atomic 包的解决方案要快得多,因为它可以生成更高的数字序列。为了更加严谨的证明这个结果,我们下面将对这两个程序进行基准测试。

性能分析

一个 benchmark 应该根据被测量的内容来解释。因此,我们假设之前的程序,有一个不断存储新配置的 数据写入器,同时也有多个不断读取配置的 数据读取器。为了涵盖更多潜在的场景,我们还将包括一个只有 数据读取器 的 benchmark,假设 Config 不经常改变。

下面是部分 benchmark 的代码:

  1. func BenchmarkMutexMultipleReaders(b *testing.B) {
  2. var lastValue uint64
  3. var mux sync.RWMutex
  4. var wg sync.WaitGroup
  5.  
  6. cfg := Config{
  7. a: []int{0, 0, 0, 0, 0, 0},
  8. }
  9.  
  10. for n := 0; n < 4; n++ {
  11. wg.Add(1)
  12.  
  13. go func() {
  14. for n := 0; n < b.N; n++ {
  15. mux.RLock()
  16. atomic.SwapUint64(&lastValue, uint64(cfg.a[0]))
  17. mux.RUnlock()
  18. }
  19. wg.Done()
  20. }()
  21. }
  22.  
  23. wg.Wait()
  24. }

执行上面的测试代码后我们可以得到如下的结果:

  1. name time/op
  2. AtomicOneWriterMultipleReaders-4 72.2ns ± 2%
  3. AtomicMultipleReaders-4 65.8ns ± 2%
  4.  
  5. MutexOneWriterMultipleReaders-4 717ns ± 3%
  6. MutexMultipleReaders-4 176ns ± 2%

基准测试证实了我们之前看到的性能情况。为了了解 mutex 的瓶颈到底在哪里,我们可以在启用 tracer 的情况下重新运行程序。

goroutines 运行时不间断,能够完成任务。对于带有 mutex 的程序的配置文件,得到的结果那是完全不同的。

现在运行时间相当零碎,这是由于停放 goroutine 的 mutex 造成的。这一点可以从 goroutine 的概览中得到证实,其中显示了同步时被阻塞的时间。

屏蔽时间大概占到三分之一的时间,这一点可以从下面的 block profile 的图中详细看到。

在这种情况下,atomic 包肯定会带来优势。但是,在某些方面可能会降低性能。例如,如果你要存储一张大地图,每次更新地图时都要复制它,这样效率就很低。

via: https://medium.com/a-journey-with-go/go-how-to-reduce-lock-contention-with-the-atomic-package-ba3b2664b549

作者:Vincent Blanchon 译者:double12gzh 校对:lxbwolf

原文地址:https://studygolang.com/articles/35385?utm_source=tuicool&utm_medium=referral

延伸 · 阅读

精彩推荐
  • Golanggo语言制作端口扫描器

    go语言制作端口扫描器

    本文给大家分享的是使用go语言编写的TCP端口扫描器,可以选择IP范围,扫描的端口,以及多线程,有需要的小伙伴可以参考下。 ...

    脚本之家3642020-04-25
  • Golanggolang json.Marshal 特殊html字符被转义的解决方法

    golang json.Marshal 特殊html字符被转义的解决方法

    今天小编就为大家分享一篇golang json.Marshal 特殊html字符被转义的解决方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 ...

    李浩的life12792020-05-27
  • GolangGolang通脉之数据类型详情

    Golang通脉之数据类型详情

    这篇文章主要介绍了Golang通脉之数据类型,在编程语言中标识符就是定义的具有某种意义的词,比如变量名、常量名、函数名等等,Go语言中标识符允许由...

    4272021-11-24
  • Golanggo日志系统logrus显示文件和行号的操作

    go日志系统logrus显示文件和行号的操作

    这篇文章主要介绍了go日志系统logrus显示文件和行号的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    SmallQinYan12302021-02-02
  • Golanggolang的httpserver优雅重启方法详解

    golang的httpserver优雅重启方法详解

    这篇文章主要给大家介绍了关于golang的httpserver优雅重启的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,...

    helight2992020-05-14
  • Golanggolang如何使用struct的tag属性的详细介绍

    golang如何使用struct的tag属性的详细介绍

    这篇文章主要介绍了golang如何使用struct的tag属性的详细介绍,从例子说起,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看...

    Go语言中文网11352020-05-21
  • Golanggolang 通过ssh代理连接mysql的操作

    golang 通过ssh代理连接mysql的操作

    这篇文章主要介绍了golang 通过ssh代理连接mysql的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    a165861639710342021-03-08
  • GolangGolang中Bit数组的实现方式

    Golang中Bit数组的实现方式

    这篇文章主要介绍了Golang中Bit数组的实现方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    天易独尊11682021-06-09