sync.Once 的用法

​ 在多数情况下,sync.Once 被用于控制变量的初始化,这个变量的读写通常遵循单例模式,满足这三个条件:

  • 当且仅当第一次读某个变量时,进行初始化(写操作)
  • 变量被初始化过程中,所有读都被阻塞(读操作;当变量初始化完成后,读操作继续进行)
  • 变量仅初始化一次,初始化完成后驻留在内存里

实例化一次客户端

​ 在标准库中不乏有大量 sync.Once 的使用案例,在 strings 包中 replace.go 里实现字符串批量替换功能时,需要预编译生成替换规则,即采用不同的替换算法并创建相关算法实例,因 strings.Replacer 实现是线程安全且支持规则复用,在第一次解析替换规则并创建对应算法实例后,可以并发的进行字符串替换操作,避免多次解析替换规则浪费资源。

先看一下 strings.Replacer 的结构定义:

1
2
3
4
5
6
// source: strings/replace.go
type Replacer struct {
	once   sync.Once // guards buildOnce method
	r      replacer
	oldnew []string
}

​ 这里定义了 once sync.Once 用来控制 r replacer 替换算法初始化,当我们使用 strings.NewReplacer 创建 strings.Replacer 时,这里采用惰性算法,并没有在这时进行 build 解析替换规则并创建对应算法实例,而是在执行替换时( Replacer.ReplaceReplacer.WriteString)进行的, r.once.Do(r.buildOnce) 使用 sync.OnceDo 方法保证只有在首次执行时才会执行 buildOnce 方法,而在 buildOnce 中调用 build 解析替换规则并创建对应算法实例,在 buildOnce 中进行赋值。

 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
// source: strings/replace.go
func NewReplacer(oldnew ...string) *Replacer {
	if len(oldnew)%2 == 1 {
		panic("strings.NewReplacer: odd argument count")
	}
	return &Replacer{oldnew: append([]string(nil), oldnew...)}
}

func (r *Replacer) buildOnce() {
	r.r = r.build()
	r.oldnew = nil
}

func (b *Replacer) build() replacer {
    ....
}

func (r *Replacer) Replace(s string) string {
	r.once.Do(r.buildOnce)
	return r.r.Replace(s)
}

func (r *Replacer) WriteString(w io.Writer, s string) (n int, err error) {
	r.once.Do(r.buildOnce)
	return r.r.WriteString(w, s)
}

​ 简单来说,once.Do 中的函数只会执行一次,并保证 once.Do 返回时,传入 Do 的函数已经执行完成。多个 goroutine 同时执行 once.Do 的时候,可以保证抢占到 once.Do 执行权的 goroutine 执行完 once.Do 后,其他 goroutine 才能得到返回。

once.Do 接收一个函数作为参数,该函数不接受任何参数,不返回任何参数。具体做什么由使用方决定,错误处理也由使用方控制,对函数初始化的结果也由使用方进行保存

资源清理退出

​ 一种错误处理的例子 exec.closeOnceexec.closeOnce 保证了重复关闭文件,永远只执行一次,并且总是返回首次关闭产生的错误信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// source: os/exec/exec.go
type closeOnce struct {
	*os.File

	once sync.Once
	err  error
}

func (c *closeOnce) Close() error {
	c.once.Do(c.close)
	return c.err
}

func (c *closeOnce) close() {
	c.err = c.File.Close()
}

nsq的停止

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// source: apps/nsqd/main.go
type program struct {
	once sync.Once
	nsqd *nsqd.NSQD
}

func main() {
	prg := &program{}
	if err := svc.Run(prg, syscall.SIGINT, syscall.SIGTERM); err != nil {
		logFatal("%s", err)
	}
}

func (p *program) Sweight() error {
	p.once.Do(func() {
		p.nsqd.Exit()
	})
	return nil
}	

常见的单例模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var (
   onceHost  sync.Once
   hostQueue *mq.PubSub
)

func getHostInstance() *mq.PubSub {

   onceHost.Do(func() {
      var err error

      hostQueue, err = mq.NewPubSub(context.Background())
      if err != nil {
         log.Error(err)
      }
   })

   return hostQueue
}

参考链接