如何确保代码在 Go 中没有数据竞争?
How to make sure code has no data races in Go?
我正在编写一个调用其他微服务的微服务,用于很少更新的数据(一天一次或一个月一次)。所以我决定创建缓存,并实现了这个接口:
type StringCache interface {
Get(string) (string, bool)
Put(string, string)
}
内部只是 map[string]cacheItem
,其中
type cacheItem struct {
data string
expire_at time.Time
}
我的同事说它不安全,我需要在我的方法中添加互斥锁,因为它会被不同的 http 处理函数实例并行使用。我有一个测试,但它没有检测到数据竞争,因为它在一个 goroutine 中使用缓存:
func TestStringCache(t *testing.T) {
testDuration := time.Millisecond * 10
cache := NewStringCache(testDuration / 2)
cache.Put("here", "this")
// Value put in cache should be in cache
res, ok := cache.Get("here")
assert.Equal(t, res, "this")
assert.True(t, ok)
// Values put in cache will eventually expire
time.Sleep(testDuration)
res, ok = cache.Get("here")
assert.Equal(t, res, "")
assert.False(t, ok)
}
所以,我的问题是:如何重写这个检测数据竞争(如果存在)的测试 运行 go test -race
?
首先,Go 中的数据竞争检测器不是某种使用静态代码分析的正式证明器,而是一种动态工具,用于检测 compiled 代码尝试在运行时检测数据竞争的特殊方法。
这意味着如果竞争检测器很幸运并且它发现了数据竞争,您应该确定在报告的位置 是 数据竞争。但这也意味着,如果实际程序流没有使某些现有数据竞争条件 发生, 竞争检测器将不会发现并报告它。
换句话说,竞争检测器没有误报,它只是一个尽力而为的工具。
因此,为了编写无竞争代码,您真的必须重新考虑您的方法。
最好从 Go race detector 作者写的 this classic essay on the topic 开始,一旦你理解了没有良性数据竞争,你基本上只是训练自己同时考虑 运行 执行的问题每次构建数据和操作数据的算法时访问您的数据。
例如,您知道(至少您 应该 知道您是否阅读了文档)使用 net/http
实现的 HTTP 服务器的每个传入请求都会被处理通过一个单独的 goroutine。
这意味着,如果你有一个中央(共享)数据结构,比如一个缓存,它被处理客户端请求的代码访问,你 do 有多个 goroutines 可能访问共享并发数据
现在,如果您有另一个 更新 数据的 goroutine,您确实有可能发生经典数据竞争:当一个 goroutine 正在更新数据时,另一个 goroutine 可能会读取它。
关于手头的问题,有两点:
首先,从不使用计时器来测试东西。这不起作用。
其次,像你这样简单的情况,只用两个goroutine就完全够了:
package main
import (
"testing"
"time"
)
type cacheItem struct {
data string
expire_at time.Time
}
type stringCache struct {
m map[string]cacheItem
exp time.Duration
}
func (sc *stringCache) Get(key string) (string, bool) {
if item, ok := sc.m[key]; !ok {
return "", false
} else {
return item.data, true
}
}
func (sc *stringCache) Put(key, data string) {
sc.m[key] = cacheItem{
data: data,
expire_at: time.Now().Add(sc.exp),
}
}
func NewStringCache(d time.Duration) *stringCache {
return &stringCache{
m: make(map[string]cacheItem),
exp: d,
}
}
func TestStringCache(t *testing.T) {
cache := NewStringCache(time.Minute)
ch := make(chan struct{})
go func() {
cache.Put("here", "this")
close(ch)
}()
_, _ = cache.Get("here")
<-ch
}
将此保存为 sc_test.go
然后
tmp$ go test -race -c -o sc_test ./sc_test.go
tmp$ ./sc_test
==================
WARNING: DATA RACE
Write at 0x00c00009e270 by goroutine 8:
runtime.mapassign_faststr()
/home/kostix/devel/golang-1.13.6/src/runtime/map_faststr.go:202 +0x0
command-line-arguments.(*stringCache).Put()
/home/kostix/tmp/sc_test.go:27 +0x144
command-line-arguments.TestStringCache.func1()
/home/kostix/tmp/sc_test.go:46 +0x62
Previous read at 0x00c00009e270 by goroutine 7:
runtime.mapaccess2_faststr()
/home/kostix/devel/golang-1.13.6/src/runtime/map_faststr.go:107 +0x0
command-line-arguments.TestStringCache()
/home/kostix/tmp/sc_test.go:19 +0x125
testing.tRunner()
/home/kostix/devel/golang-1.13.6/src/testing/testing.go:909 +0x199
Goroutine 8 (running) created at:
command-line-arguments.TestStringCache()
/home/kostix/tmp/sc_test.go:45 +0xe4
testing.tRunner()
/home/kostix/devel/golang-1.13.6/src/testing/testing.go:909 +0x199
Goroutine 7 (running) created at:
testing.(*T).Run()
/home/kostix/devel/golang-1.13.6/src/testing/testing.go:960 +0x651
testing.runTests.func1()
/home/kostix/devel/golang-1.13.6/src/testing/testing.go:1202 +0xa6
testing.tRunner()
/home/kostix/devel/golang-1.13.6/src/testing/testing.go:909 +0x199
testing.runTests()
/home/kostix/devel/golang-1.13.6/src/testing/testing.go:1200 +0x521
testing.(*M).Run()
/home/kostix/devel/golang-1.13.6/src/testing/testing.go:1117 +0x2ff
main.main()
_testmain.go:44 +0x223
==================
--- FAIL: TestStringCache (0.00s)
testing.go:853: race detected during execution of test
FAIL
我正在编写一个调用其他微服务的微服务,用于很少更新的数据(一天一次或一个月一次)。所以我决定创建缓存,并实现了这个接口:
type StringCache interface {
Get(string) (string, bool)
Put(string, string)
}
内部只是 map[string]cacheItem
,其中
type cacheItem struct {
data string
expire_at time.Time
}
我的同事说它不安全,我需要在我的方法中添加互斥锁,因为它会被不同的 http 处理函数实例并行使用。我有一个测试,但它没有检测到数据竞争,因为它在一个 goroutine 中使用缓存:
func TestStringCache(t *testing.T) {
testDuration := time.Millisecond * 10
cache := NewStringCache(testDuration / 2)
cache.Put("here", "this")
// Value put in cache should be in cache
res, ok := cache.Get("here")
assert.Equal(t, res, "this")
assert.True(t, ok)
// Values put in cache will eventually expire
time.Sleep(testDuration)
res, ok = cache.Get("here")
assert.Equal(t, res, "")
assert.False(t, ok)
}
所以,我的问题是:如何重写这个检测数据竞争(如果存在)的测试 运行 go test -race
?
首先,Go 中的数据竞争检测器不是某种使用静态代码分析的正式证明器,而是一种动态工具,用于检测 compiled 代码尝试在运行时检测数据竞争的特殊方法。
这意味着如果竞争检测器很幸运并且它发现了数据竞争,您应该确定在报告的位置 是 数据竞争。但这也意味着,如果实际程序流没有使某些现有数据竞争条件 发生, 竞争检测器将不会发现并报告它。
换句话说,竞争检测器没有误报,它只是一个尽力而为的工具。
因此,为了编写无竞争代码,您真的必须重新考虑您的方法。
最好从 Go race detector 作者写的 this classic essay on the topic 开始,一旦你理解了没有良性数据竞争,你基本上只是训练自己同时考虑 运行 执行的问题每次构建数据和操作数据的算法时访问您的数据。
例如,您知道(至少您 应该 知道您是否阅读了文档)使用 net/http
实现的 HTTP 服务器的每个传入请求都会被处理通过一个单独的 goroutine。
这意味着,如果你有一个中央(共享)数据结构,比如一个缓存,它被处理客户端请求的代码访问,你 do 有多个 goroutines 可能访问共享并发数据
现在,如果您有另一个 更新 数据的 goroutine,您确实有可能发生经典数据竞争:当一个 goroutine 正在更新数据时,另一个 goroutine 可能会读取它。
关于手头的问题,有两点:
首先,从不使用计时器来测试东西。这不起作用。
其次,像你这样简单的情况,只用两个goroutine就完全够了:
package main
import (
"testing"
"time"
)
type cacheItem struct {
data string
expire_at time.Time
}
type stringCache struct {
m map[string]cacheItem
exp time.Duration
}
func (sc *stringCache) Get(key string) (string, bool) {
if item, ok := sc.m[key]; !ok {
return "", false
} else {
return item.data, true
}
}
func (sc *stringCache) Put(key, data string) {
sc.m[key] = cacheItem{
data: data,
expire_at: time.Now().Add(sc.exp),
}
}
func NewStringCache(d time.Duration) *stringCache {
return &stringCache{
m: make(map[string]cacheItem),
exp: d,
}
}
func TestStringCache(t *testing.T) {
cache := NewStringCache(time.Minute)
ch := make(chan struct{})
go func() {
cache.Put("here", "this")
close(ch)
}()
_, _ = cache.Get("here")
<-ch
}
将此保存为 sc_test.go
然后
tmp$ go test -race -c -o sc_test ./sc_test.go
tmp$ ./sc_test
==================
WARNING: DATA RACE
Write at 0x00c00009e270 by goroutine 8:
runtime.mapassign_faststr()
/home/kostix/devel/golang-1.13.6/src/runtime/map_faststr.go:202 +0x0
command-line-arguments.(*stringCache).Put()
/home/kostix/tmp/sc_test.go:27 +0x144
command-line-arguments.TestStringCache.func1()
/home/kostix/tmp/sc_test.go:46 +0x62
Previous read at 0x00c00009e270 by goroutine 7:
runtime.mapaccess2_faststr()
/home/kostix/devel/golang-1.13.6/src/runtime/map_faststr.go:107 +0x0
command-line-arguments.TestStringCache()
/home/kostix/tmp/sc_test.go:19 +0x125
testing.tRunner()
/home/kostix/devel/golang-1.13.6/src/testing/testing.go:909 +0x199
Goroutine 8 (running) created at:
command-line-arguments.TestStringCache()
/home/kostix/tmp/sc_test.go:45 +0xe4
testing.tRunner()
/home/kostix/devel/golang-1.13.6/src/testing/testing.go:909 +0x199
Goroutine 7 (running) created at:
testing.(*T).Run()
/home/kostix/devel/golang-1.13.6/src/testing/testing.go:960 +0x651
testing.runTests.func1()
/home/kostix/devel/golang-1.13.6/src/testing/testing.go:1202 +0xa6
testing.tRunner()
/home/kostix/devel/golang-1.13.6/src/testing/testing.go:909 +0x199
testing.runTests()
/home/kostix/devel/golang-1.13.6/src/testing/testing.go:1200 +0x521
testing.(*M).Run()
/home/kostix/devel/golang-1.13.6/src/testing/testing.go:1117 +0x2ff
main.main()
_testmain.go:44 +0x223
==================
--- FAIL: TestStringCache (0.00s)
testing.go:853: race detected during execution of test
FAIL