从按值传递的结构值读取字段时的数据竞争
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`
有道理。
注:本题不是关于如何实现线程安全计数器
竞争是因为 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
为什么 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`
有道理。
注:本题不是关于如何实现线程安全计数器
竞争是因为 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