文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

如何使用 atomic 包减少锁冲突

2024-12-02 11:15

关注

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

数据竞争

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

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

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

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

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

通过上面的代码,我们做了两处改动。第一处改动在写数据前通过 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. type Config struct { 
  9.     a []int 
  10.  
  11. func main() { 
  12.     var v atomic.Value 
  13.  
  14.     // 写入数据 
  15.     go func() { 
  16.         var i int 
  17.         for { 
  18.             i++ 
  19.             cfg := Config{ 
  20.                 a: []int{i, i + 1, i + 2, i + 3, i + 4, i + 5}, 
  21.             } 
  22.             v.Store(cfg) 
  23.         } 
  24.     }() 
  25.  
  26.     // 读取数据 
  27.     var wg sync.WaitGroup 
  28.     for n := 0; n < 4; n++ { 
  29.         wg.Add(1) 
  30.         go func() { 
  31.             for n := 0; n < 100; n++ { 
  32.                 cfg := v.Load() 
  33.                 fmt.Printf("%#v\n", cfg) 
  34.             } 
  35.             wg.Done() 
  36.         }() 
  37.     } 
  38.  
  39.     wg.Wait() 

这里我们使用了 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() 

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

  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

来源:Go语言中文网内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯