当通过值传递的结构体值读取字段时,会发生数据竞争。

huangapple go评论76阅读模式
英文:

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更改为值,并使用vet -copylocks运行程序,我会得到以下警告:

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

这是有道理的。

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

链接到playground代码

英文:

Why does golang race detector complains about the following code:

package main

import (
	&quot;fmt&quot;
	&quot;sync&quot;
)

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

func NewCounter() *Counter {
	return &amp;Counter {0, &amp;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 &lt; max ; i++ {
			value := counter.get()
			fmt.Printf(&quot;counter value = %d\n&quot;, value)
			wg.Done()
		}
	}()
	// producer
	go func() {
		for i := 0; i &lt; max ; i++ {
			counter.inc()
		}
	}()

	wg.Wait()
}

When I run the code above with -race I'm getting the following warnings:

==================
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
==================

If I change func (c Counter) get() int to func (c *Counter) get() int then everything is working fine. It turns out that the receiver type for get() function should be a pointer. And I'm confused why that is. I'm aware of "-copylocks" but in this case mtx is a pointer, not value. If I change 'mtx' to be value and run program with vet -copylocks I get this warning:

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

That makes sense.

note: This question is not about how to implement thread safe counter

link to playground code

答案1

得分: 5

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

value := Counter.get(*counter)

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

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

英文:

The race is because of the value receiver for the get() method. In order to call the get() method, a copy of the struct must be passed to the method expression. The method call without the syntactic sugar looks like:

value := Counter.get(*counter)

Copying the struct entails reading the value field, which happens before the method can take the lock, which is why the race is reported on the line of the method call, rather than within the method.

This is why changing the receiver to a pointer receiver will fix the issue. Also, since all receivers need to be pointers, the mtx can be left as a sync.Mutex value so it doesn't need to be initialized.

答案2

得分: 1

正如@JimB指出的,在get()方法的情况下,传递的是一个副本,因此首先读取字段值,然后进行复制,没有任何锁定,并且由于在inc()中对相同的变量进行了修改,因此会检测到竞争条件。

为了进一步说明这一点,您还可以将字段value的类型更改为指针,即value *int,这样您就不会再看到竞争条件,因为现在只复制了指针而不是底层值。尽管如此,为了使意图更清晰,将get()的接收器类型更改为指针会更加清晰。

这是一个关于相同问题的很好的维基页面 - https://github.com/golang/go/wiki/CodeReviewComments#receiver-type

关于方法的简要评论:
https://golang.org/ref/spec#Method_values

英文:

As @JimB points out, in the case of the get() method a copy is passed in which case
the field value is read first and then copied, without any locking and since the
same variable is mutated in inc(), the race is detected.

To further illustrate this point, you could also change the type of the field value
to a pointer i.e. value *int in which case you should no longer see a race as now
only the pointer is copied and not the underlying value. That said, to make intentions
clearer, it is cleaner to change the get() receiver type to be a pointer.

Here is a good wiki around the same - https://github.com/golang/go/wiki/CodeReviewComments#receiver-type

A brief commentary on methods:
https://golang.org/ref/spec#Method_values

huangapple
  • 本文由 发表于 2017年2月4日 06:41:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/42034178.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定