sync包提供了基本的同步原语,像互斥锁(sync.Mutex)、条件变量(sync.Cond)、等待组(sync.WaitGroup)等。对于所有这些类型,有一条硬性规则需要我们遵守:不能对这些类型的变量进行复制使用。本文讨论它们的工作原理以及如果进行复制使用会导致什么问题。
下面程序实现了一个计数存储功能,并且是线程安全的。Counter
结构中的map[string]int
表示每个计数器的当前值,为了保证其并发访问操作的安全性,使用sync.Mutex保护它,Add方法实现计数增加功能。代码如下, 对counters计数放在临界区中,即放在c.mu.Lock()
和c.mu.Unlock()
中。
type Counter struct {
mu sync.Mutex
counters map[string]int
}
func NewCounter() Counter {
return Counter{counters: map[string]int{}}
}
func (c Counter) Increment(name string) {
c.mu.Lock()
defer c.mu.Unlock()
c.counters[name]++
}
下面启动两个goroutine对counter进行自增操作,在运行时加入-race参数进行数据竞争检查,看看会产生什么情况。
counter := NewCounter()
go func() {
counter.Increment("foo")
}()
go func() {
counter.Increment("bar")
}()
完整代码见https://github.com/ThomasMing0915/100-go-mistakes-code/tree/main/74,程序输出结果如下。什么?,竟然存在数据竞争?
go run -race example1.go
==================
WARNING: DATA RACE
Read at 0x00c0000a0060 by goroutine 7:
runtime.mapaccess1_faststr()
上述代码存在数据竞争的原因是互斥锁(mu)被复制了,因为Increment操作的接收者是值类型,所以当我们每次调用Increment时,它都会对当前的Counter进行复制,Counter内部的互斥锁也被复制了。sync包中的类型不能被复制使用,像下面列举的类型都是不能进行复制使用的.
既然知道了上述程序问题所在,现在的问题是如何解决呢?主要有两种解决方法。
第一种解决方法是,将Increment方法的接收者从值类型改为指针类型,代码如下。通过修改接收者类型,可以避免调用Increment时复制Counter,进而避免内部互斥锁复制。
func (c *Counter) Increment(name string) {
// Same code
}
第二种解法方法是,如果不想调整接收者的类型,可以将Counter结构体中mu字段的类型改为指针类型,代码如下。虽然Increment的接收者类型还是值类型,调用时会复制Counter结构,但是由于mu是一个指针,复制后指针指向的对象和被复制对象指针指向都是同一个对象,所以不存在数据竞争问题。
type Counter struct {
mu *sync.Mutex
counters map[string]int
}
func NewCounter() Counter {
return Counter{
mu: &sync.Mutex{},
counters: map[string]int{},
}
}
「NOTE: 在第二种解决方法中,我们将mu字段定义为指针类型,这个时候在创建Counter时需要进行初始化。如果省略它,mu的值会被初始化为指针的零值(nil),在调用c.mu.Lock()会产生panic.」
在下面的情况中,我们可能会遇到无意复制sync字段的问题,在编写程序时应该小心谨慎。
此外,使用一些静态代码检查工具linter可以扫描出这类问题。例如,使用go vet 检查前面的程序,输出结果如下:
go vet .
./example1.go:19:9: Increment passes lock by value: Counter contains sync.Mutex
总结:当多个goroutine需要访问一个公共的sync包中对象时,我们必须确保它们都依赖于同一个实例。该规则适用于sync包定义的所有类型,使用指针而不是值是解决这种问题的一个方法:将结构体中用到的sync包中类型的字段定义为指针类型,或者使用结构体的指针对象。