您现在的位置是:亿华云 > 系统运维

Go:有了 Sync 为什么还有 Atomic?

亿华云2025-10-03 06:59:01【系统运维】4人已围观

简介Go 是一种擅长并发的语言,启动新的 goroutine 就像输入 “go” 一样简单。随着你发现自己构建的系统越来越复杂,正确保护对共享资源的访问以防止竞争条件变得极其重要。此类资源可能包括可即时更

Go 是有S有一种擅长并发的语言,启动新的什还 goroutine 就像输入 “go” 一样简单。随着你发现自己构建的有S有系统越来越复杂,正确保护对共享资源的什还访问以防止竞争条件变得极其重要。此类资源可能包括可即时更新的有S有配置(例如功能标志)、内部状态(例如断路器状态)等。什还

01 什么是有S有竞态条件?

对于大多数读者来说,这可能是什还基础知识,但由于本文的有S有其余部分取决于对竞态条件的理解,因此有必要进行简短的什还复习。竞态条件是有S有一种情况,在这种情况下,什还程序的有S有行为取决于其他不可控事件的顺序或时间。在大多数情况下,什还这种情况是有S有一个错误,因为可能会发生不希望的结果。

举个具体的例子或许更容易理解:

// race_condition_test.go package main import (  "fmt"  "sort"  "sync"  "testing" ) func Test_RaceCondition(t *testing.T) {   var s = make([]int, 0)  wg := sync.WaitGroup{ }  // spawn 10 goroutines to modify the slice in parallel  for i := 0; i < 10; i++ {    wg.Add(1)   go func(i int) {     defer wg.Done()    s = append(s, i) //add a new item to the slice   }(i)  }  wg.Wait()  sort.Ints(s) //sort the response to have comparable results  fmt.Println(s) } 

执行一:

$ go test -v race_condition_test.go === RUN   Test_RaceCondition [0 1 2 3 4 5 6 7 8 9] --- PASS: Test_RaceCondition (0.00s) 

这里看起来一切都很好。高防服务器这是我们预期的输出。该程序迭代了 10 次,并在每次迭代时将索引添加到切片中。

执行二:

=== RUN Test_RaceCondition [0 3] --- PASS: Test_RaceCondition (0.00s) 

等等,这里发生了什么?这次我们的响应切片中只有两个元素。这是因为切片的内容 s 在加载和修改之间发生了变化,导致程序覆盖了一些结果。这种特殊的竞态条件是由数据竞争引起的,在这种情况下,多个 goroutine 尝试同时访问特定的共享变量,并且这些 goroutine 中的至少一个尝试修改它。(注意,以上结果并非一定如此,每次运行结果可能都不相同)

如果你使用 -race 标志执行测试,go 甚至会告诉你存在数据竞争并帮助你准确定位:

$ go test race_condition_test.go -race ================== WARNING: DATA RACE Read at 0x00c000132048 by goroutine 9:   command-line-arguments.Test_RaceCondition.func1()       /home/sfinlay/go/src/benchmarks/race_condition_test.go:20 +0xb4   command-line-arguments.Test_RaceCondition·dwrap·1()       /home/sfinlay/go/src/benchmarks/race_condition_test.go:21 +0x47 Previous write at 0x00c000132048 by goroutine 8:   command-line-arguments.Test_RaceCondition.func1()       /home/sfinlay/go/src/benchmarks/race_condition_test.go:20 +0x136   command-line-arguments.Test_RaceCondition·dwrap·1()       /home/sfinlay/go/src/benchmarks/race_condition_test.go:21 +0x47 Goroutine 9 (running) created at:   command-line-arguments.Test_RaceCondition()       /home/sfinlay/go/src/benchmarks/race_condition_test.go:18 +0xc5   testing.tRunner()       /usr/local/go/src/testing/testing.go:1259 +0x22f   testing.(*T).Run·dwrap·21()       /usr/local/go/src/testing/testing.go:1306 +0x47 Goroutine 8 (finished) created at:   command-line-arguments.Test_RaceCondition()       /home/sfinlay/go/src/benchmarks/race_condition_test.go:18 +0xc5   testing.tRunner()       /usr/local/go/src/testing/testing.go:1259 +0x22f   testing.(*T).Run·dwrap·21()       /usr/local/go/src/testing/testing.go:1306 +0x47 ================== 

02 并发控制

保护对这些共享资源的访问通常涉及常见的内存同步机制,例如通道或互斥锁。

这是将竞态条件调整为使用互斥锁的相同测试用例:

func Test_NoRaceCondition(t *testing.T) {   var s = make([]int, 0)  m := sync.Mutex{ }  wg := sync.WaitGroup{ }  // spawn 10 goroutines to modify the slice in parallel  for i := 0; i < 10; i++ {    wg.Add(1)   go func(i int) {     m.Lock()    defer wg.Done()    defer m.Unlock()    s = append(s, i)   }(i)  }  wg.Wait()  sort.Ints(s) //sort the response to have comparable results  fmt.Println(s) } 

这次它始终返回所有 10 个整数,因为它确保每个 goroutine 仅在没有其他人执行时才读写切片。如果第二个 goroutine 同时尝试获取锁,云南idc服务商它必须等到前一个 goroutine 完成(即直到它解锁)。

然而,对于高吞吐量系统,性能变得非常重要,因此减少锁争用(即一个进程或线程试图获取另一个进程或线程持有的锁的情况)变得更加重要。执行此操作的最基本方法之一是使用读写锁 ( sync.RWMutex) 而不是标准 sync.Mutex,但是 Go 还提供了一些原子内存原语即 atomic 包。

03 原子

Go 的 atomic 包提供了用于实现同步算法的低级原子内存原语。这听起来像是我们需要的东西,所以让我们尝试用 atomic 重写该测试:

import "sync/atomic" func Test_RaceCondition_Atomic(t *testing.T) {   var s = atomic.Value{ }  s.Store([]int{ }) // store empty slice as the base  wg := sync.WaitGroup{ }  // spawn 10 goroutines to modify the slice in parallel  for i := 0; i < 10; i++ {    wg.Add(1)   go func(i int) {     defer wg.Done()    s1 := s.Load().([]int)    s.Store(append(s1, i)) //replace the slice with a new one containing the new item   }(i)  }  wg.Wait()  s1 := s.Load().([]int)  sort.Ints(s1) //sort the response to have comparable results  fmt.Println(s1) } 

执行结果:

=== RUN Test_RaceCondition_Atomic [1 3] --- PASS: Test_RaceCondition_Atomic (0.00s) 

什么?这和我们之前遇到的问题完全一样,那么这个包有什么好处呢?

04 读取-复制-更新

atomic 不是灵丹妙药,它显然不能替代互斥锁,但是当涉及到可以使用读取-复制-更新[1]模式管理的共享资源时,它非常出色。在这种技术中,我们通过引用获取当前值,当我们想要更新它时,我们不修改原始值,而是源码下载替换指针(因此没有人访问另一个线程可能访问的相同资源)。前面的示例无法使用此模式实现,因为它应该随着时间的推移扩展现有资源而不是完全替换其内容,但在许多情况下,读取-复制-更新是完美的。

这是一个基本示例,我们可以在其中获取和存储布尔值(例如,对于功能标志很有用)。在这个例子中,我们正在执行一个并行基准测试,比较原子和读写互斥:

package main import (  "sync"  "sync/atomic"  "testing" ) type AtomicValue struct{   value atomic.Value } func (b *AtomicValue) Get() bool {   return b.value.Load().(bool) } func (b *AtomicValue) Set(value bool) {   b.value.Store(value) } func BenchmarkAtomicValue_Get(b *testing.B) {   atomB := AtomicValue{ }  atomB.value.Store(false)  b.RunParallel(func(pb *testing.PB) {    for pb.Next() {     atomB.Get()   }  }) } /

很赞哦!(1)