从按值传递的结构值读取字段时的数据竞争

Data race when reading field from struct value passed by value

为什么 golang 竞争检测器会抱怨以下代码:

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    value int
    mtx     *sync.Mutex
}

func NewCounter() *Counter {
    return &Counter {0, &sync.Mutex{}}
}

func (c *Counter) inc() {
    c.mtx.Lock()
    c.value++
    c.mtx.Unlock()
}

func (c Counter) get() int {
    c.mtx.Lock()
    res := c.value
    c.mtx.Unlock()
    return res
}

func main() {
    var wg sync.WaitGroup
    counter := NewCounter()
    max := 100
    wg.Add(max)

    // consumer
    go func() {
        for i := 0; i < max ; i++ {
            value := counter.get()
            fmt.Printf("counter value = %d\n", value)
            wg.Done()
        }
    }()
    // producer
    go func() {
        for i := 0; i < max ; i++ {
            counter.inc()
        }
    }()

    wg.Wait()
}

当我 运行 使用 -race 上面的代码时,我收到以下警告:

==================
WARNING: DATA RACE
Read at 0x00c0420042b0 by goroutine 6:
  main.main.func1()
      main.go:39 +0x72

Previous write at 0x00c0420042b0 by goroutine 7:
  main.(*Counter).inc()
      main.go:19 +0x8b
  main.main.func2()
      main.go:47 +0x50

Goroutine 6 (running) created at:
  main.main()
      main.go:43 +0x167

Goroutine 7 (running) created at:
  main.main()
      main.go:49 +0x192
==================

如果我将 func (c Counter) get() int 更改为 func (c *Counter) get() int 那么一切正常。事实证明,get() 函数的接收者类型应该是一个指针。我很困惑为什么会这样。我知道“-copylocks”,但在这种情况下 mtx 是一个指针,而不是值。如果我将 'mtx' 更改为 value 并使用 vet -copylocks 更改 运行 程序,我会收到此警告:

main.go:23: get passes lock by value: main.Counter contains sync.Mutex`

有道理。

注:本题不是关于如何实现线程安全计数器

link to playground code

竞争是因为 get() 方法的值接收者。为了调用 get() 方法,必须将结构的副本传递给方法表达式。没有语法糖的方法调用如下所示:

value := Counter.get(*counter)

复制结构需要读取 value 字段,这发生在方法可以获取锁之前,这就是为什么在方法调用行而不是方法内部报告竞争。

这就是将接收器更改为指针接收器可以解决问题的原因。此外,由于所有接收器都需要是指针,因此 mtx 可以保留为 sync.Mutex 值,因此不需要对其进行初始化。

正如@JimB 指出的那样,在 get() 方法的情况下传递了一个副本,在这种情况下 首先读取字段值然后复制,没有任何锁定并且因为 相同的变量在 inc() 中发生突变,检测到竞争。

为了进一步说明这一点,您还可以更改字段的类型 value 指向一个指针,即 value *int 在这种情况下你不应该再像现在这样看到比赛 仅复制指针而不复制基础值。就是说,要立志 更清晰,将 get() 接收器类型更改为指针更清晰。

这是一个很好的 wiki - https://github.com/golang/go/wiki/CodeReviewComments#receiver-type

方法简述: https://golang.org/ref/spec#Method_values