Golang 提供了内存同步机制,在共享内存场景,使用互斥锁(Mutex)可以对抗数据竞争,在提供两种互斥锁之外,也可以通过原子化操作(Atomic)来解决问题, 相较于使用互斥锁,原子化操作拥有更好的性能。
数据竞争示例
这个简单的示例模拟了一个不断变化的配置,通过多个协程来读取。
package main
import (
"fmt"
"sync"
)
type Config struct {
a []int
}
func main() {
cfg := &Config{}
// Writer
go func () {
i := 0
for {
i++
cfg.a = []int{i, i+1, i+2, i+3, i+4, i+5}
}
}()
// Reader
var wg sync.WaitGroup
for n:= 0; n < 4; n++ {
wg.Add(1)
go func() {
for n := 0; n < 1000; n++ {
fmt.Println(cfg)
}
wg.Done()
}()
}
wg.Wait()
}
以下是部分结果
...
&{[250463 250467 250470 250473 250476 250479]}
&{[250524 250526 250529 250532 250536 250539]}
&{[250556 250561 250564 250568 250571 250574]}
按照预期,数据应该是 1 2 3 4 5 6
这样连续的数值,而现在的结果跳跃且不确定,进一步说明以下赋值语句不是原子操作,在赋值过程中,变量 i
的值已经发生了变化。
cfg.a = []int{i, i+1, i+2, i+3, i+4, i+5}
解决方案一:互斥锁(Mutex)
使用互斥锁解决数据竞争问题
test-mutex.go
package main
import (
"time"
"fmt"
"sync"
)
type Config struct {
a []int
}
func main() {
start := time.Now()
cfg := &Config{}
var lock sync.RWMutex
// Writer
go func () {
i := 0
for {
i++
lock.Lock()
cfg.a = []int{i, i+1, i+2, i+3, i+4, i+5}
lock.Unlock()
}
}()
// Reader
var wg sync.WaitGroup
for n:= 0; n < 4; n++ {
wg.Add(1)
go func() {
for n := 0; n < 1000; n++ {
lock.RLock()
fmt.Println(cfg)
lock.RUnlock()
}
wg.Done()
}()
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}
部分结果如下,可以看到同一组配置的数值符合预期。
...
&{[1028 1029 1030 1031 1032 1033]}
&{[1029 1030 1031 1032 1033 1034]}
&{[1030 1031 1032 1033 1034 1035]}
使用互斥锁简单粗暴,在需要保障原子化操作的部分加锁,在多个协程的场景下就能避免数据竞争,一般情况下使用锁也是推荐的方式,足够简单且不易出错。
同时,Golang 提供的 atomic
包也值得我们学习和了解,原子操作比锁的性能更好,在性能要求高的场景下是更好的选择。
解决方案二:原子化操作(atomic)
使用 atomic
包提供的方法可以方便的实现原子化操作。
test-atomic.go
package main
import (
"time"
"fmt"
"sync"
"sync/atomic"
)
type Config struct {
a []int
}
func main() {
start := time.Now()
var v atomic.Value
// Writer
go func () {
i := 0
for {
i++
cfg := &Config{
a: []int{i, i+1, i+2, i+3, i+4, i+5},
}
v.Store(cfg)
}
}()
// Reader
var wg sync.WaitGroup
for n:= 0; n < 4; n++ {
wg.Add(1)
go func() {
for n := 0; n < 1000; n++ {
cfg := v.Load()
fmt.Println(cfg)
}
wg.Done()
}()
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}
结果连续,符合预期
...
&{[629131 629132 629133 629134 629135 629136]}
&{[629183 629184 629185 629186 629187 629188]}
&{[629210 629211 629212 629213 629214 629215]}
性能比较
将 Mutex 和 Atomic 版本的代码 fmt.Println
打印去掉,执行可以观察到性能差异
code/go-proj/test via 🐹 v1.17.4
➜ go run test-mutex.go
961.007µs
code/go-proj/test via 🐹 v1.17.4
➜ go run test-atomic.go
42.664µs
参考