您现在的位置是:亿华云 > 系统运维
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)
上一篇: 数据中心在云端和未来的优势
下一篇: 如何提高数据中心的安全性?