Golang锁失效原因之value receiver
先说结论golang中,值类型在作为方法参数和方法的接受者的时候,都需要进行值的拷贝,所以,使用值类型的时候要多加注意。 对于方法的接受者,如果方法需要修改接受者的某个变量值,那么就应该把接受者设计成pointer receiver,否则对于receiver变量的修改将无效。 问题由来今天群里有人发了下面的代码: type data struct { sync.Mutex } func (d data) test(s string) { d.Lock() defer func() { d.Unlock() println("success") }() for i := 0; i < 5; i++ { fmt.Println(s,i) time.Sleep(time.Second) } } func main() { var wg sync.WaitGroup wg.Add(2) var d data go func() { defer wg.Done() d.test("read") }() go func() { defer wg.Done() d.test("write") }() wg.Wait() } 这段代码的运行结果如下: write 0 read 0 read 1 write 1 read 2 write 2 read 3 write 3 write 4 read 4 success success 对此他的疑问是:为什么会是这样呢?read和write为什么是交替执行?程序里面加了锁,锁为什么没生效呢? 因为如果锁生效,结果应该是先输出所有的write或者read,然后再输出另外一个。 问题原因先看代码,上面代码中方法 原因验证我们可以通过输出一下对象的地址来看一下。 把test方法改一下: func (d data) test(s string) { d.Lock() defer func() { d.Unlock() println("success") }() for i := 0; i < 5; i++ { fmt.Printf("%s,%d,object addr: %p n",s,i,&d) time.Sleep(time.Second) } } 我们通过 write,object addr: 0xc82000ae78 read,object addr: 0xc8200ce000 read,1,object addr: 0xc8200ce000 write,2,object addr: 0xc82000ae78 write,3,4,object addr: 0xc82000ae78 success success 可见,输出的d的地址并不相同。也就是说,实际执行的时候,是有两个data对象的,所以锁也不同,达不到公用锁的目的,所以输出结果就是乱序的。 是不是与匿名组合有关?data的定义: type data struct { sync.Mutex } 结构体中的 type data struct { lock sync.Mutex } func (d data) test(s string) { d.lock.Lock() defer func() { d.lock.Unlock() println("success") }() for i := 0; i < 5; i++ { fmt.Printf("%s,object addr: %p,lock address: %p n",&d,&(d.lock)) time.Sleep(time.Second) } } 运行结果如下: write,object addr: 0xc82000ae78,lock address: 0xc82000ae78 read,object addr: 0xc8200ce000,lock address: 0xc8200ce000 read,lock address: 0xc8200ce000 write,lock address: 0xc82000ae78 write,lock address: 0xc82000ae78 success success 另一个解决办法对于这个问题,除了把test方法的receiver改成pointer receiver之外,还有没有其他办法呢?答案是肯定的,只需要把匿名组合中的锁改成data结构体中显示变量,然后把锁由值类型改成引用类型即可。代码如下: type data struct { lock *sync.Mutex } main方法中把 //var d data var d data = data{lock:&sync.Mutex{}} 完整代码如下: type data struct { lock *sync.Mutex } func (d data) test(s string) { d.lock.Lock() defer func() { d.lock.Unlock() println("success") }() for i := 0; i < 5; i++ { fmt.Printf("%s,(d.lock)) time.Sleep(time.Second) } } func main() { var wg sync.WaitGroup wg.Add(2) //var d data var d data = data{lock:&sync.Mutex{}} go func() { defer wg.Done() d.test("read") }() go func() { defer wg.Done() d.test("write") }() wg.Wait() } 运行结果: write,object addr: 0xc82002e028,lock address: 0xc82000ae78 success read,object addr: 0xc8200d8000,lock address: 0xc82000ae78 success 由上面运行结果可以看出,data对象d在实际运行过程中依然是两个对象,但是它们的锁却是相同的,这样也能达到同步的目的。 为什么呢? 因为虽然值类型在方法传递的时候会进行一次拷贝,但是对于指针类型的字段来说,拷贝的是指针的地址,所以两个地址实际上是一样的,都指向同一把锁。 data的值是何时拷贝的呢?golang中,方法传递是值传递,在作为方法参数的时候,如果是值类型,将会对数据进行一次拷贝,这样在方法中对于参数的修改不会影响原来的值。 但是在第一个版本代码的main方法中,并没有对data类型变量d进行参数传递,那么d是如何被拷贝的呢?我们输出一下d的地址: type data struct { sync.Mutex } func (d data) test(s string) { d.Lock() defer func() { d.Unlock() println("success") }() for i := 0; i < 5; i++ { fmt.Printf("%s,&d) time.Sleep(time.Second) } } func main() { var wg sync.WaitGroup wg.Add(2) var d data fmt.Printf("initial address of d: %p n",&d) go func() { defer wg.Done() fmt.Printf("1 address of d: %p n",&d) d.test("read") }() go func() { defer wg.Done() fmt.Printf("2 address of d: %p n",&d) d.test("write") }() wg.Wait() } 结果如下: initial address of d: 0xc820072da8 2 address of d: 0xc820072da8 write,object addr: 0xc820072e60 1 address of d: 0xc820072da8 read,object addr: 0xc82000a0e0 write,object addr: 0xc820072e60 read,object addr: 0xc82000a0e0 read,object addr: 0xc820072e60 success success 通过结果我们发现,在main方法中,两个go关键字开始的goroutine中,输出的d的地址都是相同的,都为“0xc820072da8”,而真正执行test方法之后,输出的地址却成了“0xc82000a0e0”和“0xc820072e60”。显然,d是进行了拷贝的。我们保持其他代码不变,把goroutine去掉,用单线程试一下: func main() { var d data fmt.Printf("initial address of d: %p n",&d) d.test("read") d.test("write") } 输出结果: initial address of d: 0xc82000ae78 read,object addr: 0xc82000af20 read,object addr: 0xc82000af20 success write,object addr: 0xc8200cc038 write,object addr: 0xc8200cc038 success 可以看到,同样的,一共出现了三个不同的地址,也就是说,test方法调用了两次,每次都是在不同的data对象上执行的。 那么我们可以得出结论:test方法在执行的时候,它的receiver如果是值类型,那么每次方法执行也需要进行一次拷贝。 总结一下:对于值类型来说,无论是作为参数传递给其他方法还是作为方法的receiver,都要进行值的拷贝。(因为值拷贝的目的就是为了避免对于值的修改会影响原来的值,所以对于方法的接受者或者方法参数来讲,处理逻辑都是一样的。) 总结:golang中,值类型在作为方法参数和方法的接受者的时候,都需要进行值的拷贝,所以,使用值类型的时候要多加注意。 对于方法的接受者,如果方法需要修改接受者的某个变量值,那么就应该把接受者设计成pointer receiver,否则对于receiver变量的修改将无效。 比如: type Person struct { name string } func (p Person) change(newName string) { p.name = newName } func main() { var p Person = Person{"jason"} p.change("john") fmt.Println(p.name) } 输出结果为 另:关于value receiver 和 pointer receiver可以参考golang官方的Effective Go中的说明:https://golang.org/doc/effective_go.html#pointers_vs_values (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |