1、尽量减少锁的持有时间¶

尽量减少锁的持有时间,毕竟使用锁是有代价的,通过减少锁的持有时间来减轻这个代价:

  • 细化锁的粒度。通过细化锁的粒度来减少锁的持有时间以及避免在持有锁操作的时候做各种耗时的操作。
  • 不要在持有锁的时候做 IO
    操作。尽量只通过持有锁来保护 IO 操作需要的资源而不是 IO 操作本身:
func doSomething() {
    m.Lock()
    item := ...
    http.Get()  // 各种耗时的 IO 操作
    m.Unlock()
}

// 改为
func doSomething() {
    m.Lock()
    item := ...
    m.Unlock()

    http.Get()
}

2、善用 defer 来确保在函数内正确释放了锁¶

尤其是在那些内部有好几个通过 if err != nil 判断来提前返回的函数中,通过defer 可以确保不会遗漏释放锁操作,避免出现死锁问题,以及避免函数内非预期的 panic 导致死锁的问题:

func doSomething() {
    m.Lock()
    defer m.Unlock()

    err := ...
    if err != nil {
        return
    }

    err = ...
    if err != nil {
        return
    }

    ...
    return
}

不过使用 defer 的时候也要注意别因为习惯性的 defer m.Unlock() 导致无意中在持有锁的时候做了 IO 操作,出现了非预期的持有锁时间太长的问题。

// 非预期的在持有锁期间做 IO 操作
func doSomething() {
    m.Lock()
    defer m.Unlock()

    item := ...
    http.Get()  // 各种耗时的 IO 操作
}

defer 其实是有点 性能 消耗 的,需要取舍下酌情使用。

3、在适当时候使用 RWMutex¶

当确定操作不会修改保护的资源时,可以使用 RWMutex 来减少锁等待时间(不同的 goroutine 可以同时持有 RLock, 但是 Lock 限制了只能有一个 goroutine 持有 Lock):

func nickName() string {
    rw.RLock()
    defer rw.RUnlock()

    return name
}

func SetName(s string) string {
    rw.Lock()
    defer rw.Unlock()

    name = s
}

4、copy 结构体操作可能导致非预期的死锁¶

copy 结构体时,如果结构体中有锁的话,记得重新初始化一个锁对象,否则会出现非预期的死锁:

package main

 import (
     "fmt"
     "sync"
 )

 type User struct {
     sync.Mutex

     name string
 }

 func main() {
     u1 := &User{name: "test"}
     u1.Lock()
     defer u1.Unlock()

     tmp := *u1
     u2 := &tmp
     // u2.Mutex = sync.Mutex{} // 没有这一行就会死锁

     fmt.Printf("%#p\n", u1)
     fmt.Printf("%#p\n", u2)

     u2.Lock()
     defer u2.Unlock()
 }

5、使用 go vet 工具检查代码中锁的使用问题¶

可以通过 vet 这个命令行来检查上面的锁 copy 的问题。比如上面的例子的检查结果如下::

$ go vet main.go
# command-line-arguments
./main.go:19:9: assignment copies lock value to tmp: command-line-arguments.User

可以看到 vet 提示 19 行那里的 copy 操作中 copy 了一个锁。

BTW,使用 go vet 命令对整个项目进行检查时,可以通过 go vet $(go list ./… | grep -v /vendor/) 这个命令忽略掉 vendor 下的包。

6、build/test 时使用 -race 参数以便运行时检测数据竞争问题¶

可以在执行 go build 或 go test 时增加一个 -race 参数来开启数据竞争检测功能,通过这种方式来实现在本地开发环境/CI/测试环境阶段发现程序中可能存在的数据竞争问题:

package main

import (
    "fmt"
    "sync"
)

type Todo struct {
    sync.Mutex

    tasks []string
}

func (t *Todo) do() {
    for _, task := range t.tasks {
        fmt.Println(task)
    }
}

func (t *Todo) Add(task string) {
    t.Lock()
    defer t.Unlock()

    t.tasks = append(t.tasks, task)
}

func main() {
    t := &Todo{}

    for i := 0; i < 2; i++ {
        go t.Add(fmt.Sprintf("%d", i))
    }
    for i := 0; i < 2; i++ {
        t.do()
    }
}

-race 参数可以开启数据竞争检测

$ go build -race -o main .

7、使用 go-deadlock 检测死锁或锁等待问题¶

上面说的在持有锁的时候做 IO 操作或其他非预期的耗时超时的问题,一方面需要在写程序的时候注意一下,另一方面也有可能是无意中代入进去的(比如上面提到的习惯性 defer 导致的)。对于那些无意中代入进去的锁等待的问题人为的去 review 的话通常很难发现,此时就需要用工具来检测了。恰好有一个叫go-deadlock 的工具可以实现这个功能。

package main

import (
    "net/http"
    "time"

    sync "github.com/sasha-s/go-deadlock"
)

var mu sync.Mutex
var url = "http://baidu.com:90"

func do() {
    mu.Lock()
    defer mu.Unlock()

    u := url
    http.Get(u)  // 非预期的在持有锁期间做 IO 操作,导致锁等待时间变长
}

func main() {
    // 检测超过 100 ms 的锁等待
    sync.Opts.DeadlockTimeout = time.Millisecond * 100

    var wg sync.WaitGroup
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            do()
        }()
    }

    wg.Wait()
}

执行结果:

$ go run main.go
POTENTIAL DEADLOCK:
Previous place where the lock was grabbed
goroutine 36 lock 0x1483b90
main.go:14 main.do { mu.Lock() } <<<<<
main.go:30 main.main.func1 { do() }

Have been trying to lock it again for more than 100ms
goroutine 35 lock 0x1483b90
main.go:14 main.do { mu.Lock() } <<<<<
main.go:30 main.main.func1 { do() }

Here is what goroutine 36 doing now
goroutine 36 [select]:

通过上面的输出可以知道 goroutine 36 持有锁的时间过长导致其他 goroutine 获取锁的等待时间超过了 100 ms ,并且 goroutine 36 在持有锁期间正在做 18 行的 http 操作。可以看到 go-deadlock 在优化锁等待时间方面有很大的帮助,可以帮助我们及时发现异常的锁使用姿势。