介绍

sync.Once 是 Go 官方自带的标准库,实现了 exactly once 的功能。通过使用 sync.Once 我们可以很方便地实现单例模式,确保对象只被初始化一次。

首先看一个 sync.Once 的 Go 官方例子,源码链接在这里

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var once sync.Once
onceBody := func() {
    fmt.Println("Only once")
}
done := make(chan bool)
for i := 0; i < 10; i++ {
	go func() {
		once.Do(onceBody)
		done <- true
	}()
}
for i := 0; i < 10; i++ {
	<-done
}

运行程序,输出为:

1
Only once

可见,即便我们并发执行了 10 个协程,onceBody方法依然只会执行一次,这是如何做到的的呢?

源码走读

Go 语言版本:1.14

源码路径:src/sync/once.go

sync.Once 通过一个锁变量和原子变量保障最多执行一次,下面开始撸代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package sync

import (
	"sync/atomic"
)

// Once 对象结构
type Once struct {
    // done 标识被执行的状态:0 为默认值,表示还未执行;1 表示已被执行
    done uint32
    
    // m 为互斥锁,控制着临界值的进入,保证同一时间点最多有一个 func 在执行
	m    Mutex
}

// 对外可调用方法,
func (o *Once) Do(f func()) {
    // 原子获取 done 状态只有是 0 状态才允许执行
	if atomic.LoadUint32(&o.done) == 0 {
		o.doSlow(f)
	}
}

// 获取锁,并执行 f
func (o *Once) doSlow(f func()) {
    // 获取互斥锁,避免并发问题
	o.m.Lock()
    defer o.m.Unlock()
    
    // 再次检查是否已经执行完
	if o.done == 0 {
        // 执行 f 函数后将 done 设置为 1,哪怕 f 发生 panic 恐慌
        defer atomic.StoreUint32(&o.done, 1)
        //开始执行
		f()
	}
}

短短 20 行左右的代码,隐藏的信息量还是挺大的,这里还有几个知识点需要注意:

  • f 函数是同步执行的,也就是说可能存在阻塞问题
  • f 函数里面发生 panic,仍会被标识为已完成