首页 > 解决方案 > 如何确保代码在 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

标签: gorace-conditiongoroutine

解决方案


首先,Go 中的数据竞争检测器不是某种形式的证明器,它使用静态代码分析,而是一种动态工具,它以一种特殊的方式检测编译后的代码,以尝试在运行时检测数据竞争。
这意味着如果竞争检测器很幸运并且发现了数据竞争,那么您应该确保在报告的位置存在数据竞争。但这也意味着,如果实际的程序流程没有使某些现有的数据竞争条件发生,竞争检测器将不会发现并报告它。
换句话说,种族检测器没有误报,但它只是一个尽力而为的工具。

所以,为了编写无竞争的代码,你真的必须重新考虑你的方法。
最好从Go 竞赛检测器的作者撰写的关于该主题的经典文章开始,一旦你意识到不存在良性数据竞赛,你基本上只是训练自己考虑并发运行执行任务来访问你的数据每次您构建数据和算法来操作它时。

例如,您知道(至少您应该知道是否阅读过文档)每个传入的 HTTP 服务器请求net/http都由一个单独的 goroutine 处理。
这意味着,如果您有一个中央(共享)数据结构,例如缓存,该数据结构将由处理客户端请求的代码访问,那么您确实有多个 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

推荐阅读