【问题标题】:Data race when reading field from struct value passed by value从按值传递的结构值中读取字段时的数据竞争
【发布时间】:2017-02-03 22:41:44
【问题描述】:

为什么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 pass lock by value: main.Counter contains sync.Mutex`

这是有道理的。

注意:这个问题不是关于如何实现线程安全计数器

link to playground code

【问题讨论】:

    标签: go race-condition


    【解决方案1】:

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

    value := Counter.get(*counter)
    

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

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

    【讨论】:

      【解决方案2】:

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

      为了进一步说明这一点,您还可以更改字段 value 的类型 指向一个指针,即value *int,在这种情况下,您不应再像现在一样看到比赛 只有指针被复制,而不是基础值。也就是说,要做出意图 更清楚,将get()接收器类型更改为指针更简洁。

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

      对方法的简要说明: https://golang.org/ref/spec#Method_values

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-07-16
        • 2017-03-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多