GoLang 的锁“失效”

213 阅读1分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

 先看一下下面这段代码,我们的 Echo 方法通过锁去控制并发访问,期望 type 和 type___ 各自连续输出 10 次,但是运行结果真的符合我们预期么?

package main

import (
	"fmt"
	"sync"
	"time"
)

type Data struct {
	sync.Mutex
}

func (d Data) Echo(t string) {
	d.Lock()
	for i := 1; i <= 10; i++ {
		fmt.Printf("%s: %d\n", t, i)
		time.Sleep(time.Second * 1)
	}
	d.Unlock()
}

func main() {
	d := Data{}
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		d.Echo("type")
		wg.Done()
	}()
	go func() {
		d.Echo("type___")
		wg.Done()
	}()
	wg.Wait()
}

我们可以看到结果并不符合预期,最终会交叉输出:

$ go run main.go 
type___: 1
type: 1
type___: 2
type: 2
type___: 3
type: 3
type: 4
type___: 4
type: 5
type___: 5
type___: 6
type: 6
type___: 7
type: 7
type: 8
type___: 8
type___: 9
type: 9
type: 10
type___: 10

我们明明加了互斥锁,为什么没有限制并发访问,看起来锁 “失效” 了,造成这个现象的原因是什么呢?我们先看下 Mutex 的结构:

type Mutex struct {
	state int32 //记录锁状态
	sema  uint32 //控制锁状态信号量
}

可以看到 state 和 sema 都是值类型,大家想到原因了吗?再给大家一个提示:

GoLang 中定义结构体成员方法时,底层实际上是把接收者作为第一个参数传递进去了,本质还是一个普通方法。

有思路了吗?没错,既然把接收者作为参数传递给方法了,那么当我们像上面代码一样定义值接收者时,结构体会 copy 一个新变量传递给函数,实际上此时用到的 Mutex 不是同一个变量,可以打印指针看一下:

func (d Data) Echo(t string) {
	fmt.Printf("%p\n", &d.Mutex)
	d.Lock()
	for i := 1; i <= 10; i++ {
		fmt.Printf("%s: %d\n", t, i)
		time.Sleep(time.Second * 1)
	}
	d.Unlock()
}

$ go run main.go 
0xc0000ba008
0xc000100000
type___: 1
type: 1

所以解决的思路就是将方法接收者改成指针类型,这样在 copy 结构体的时候,实际上是复制的锁指针,就可以保证用的是同一个锁结构,像下面这样:

func (d *Data) Echo(t string) {
	fmt.Printf("%p\n", &d.Mutex)
	d.Lock()
	for i := 1; i <= 10; i++ {
		fmt.Printf("%s: %d\n", t, i)
		time.Sleep(time.Second * 1)
	}
	d.Unlock()
}

看下结果,地址是同一个,并且打印顺序符合预期:

$ go run main.go 
0xc00001c0b0
0xc00001c0b0
type___: 1
type___: 2
type___: 3
type___: 4
type___: 5
type___: 6
type___: 7
type___: 8
type___: 9
type___: 10
type: 1
type: 2
type: 3
type: 4
type: 5
type: 6
type: 7
type: 8
type: 9
type: 10

既然编译的时候不会报错,那么我们该如何及时发现代码问题呢,可以用两个工具去检测,go vet 和 golangci-lint:

$ go vet main.go 
# command-line-arguments
./main.go:13:9: Echo passes lock by value: command-line-arguments.Data


$ golangci-lint main.go 
Error: unknown command "main.go" for "golangci-lint"
Run 'golangci-lint --help' for usage.
failed executing command with error unknown command "main.go" for "golangci-lint"