Go语言在设计上对同步(Synchronization,数据同步和线程同步)提供大量的支持,好比 goroutine和channel同步原语,库层面有html
注意:__当我说“类”时,是指 Go 里的 struct(__单身狗要有面向“对象”编程的觉悟__)。git
Go语言里对同步的支持主要有五类应用场景:github
注意:__这里当我说”线程”时,了解Go的同窗能够自动映射到 “goroutine”(协程)。golang
关于 1和2,经过官方文档了解其用法和实现。本系列的主角是 sync 下的工工具类,从 sync.Once 开始。内容分两部分:sync.Once 用法和sync.Once 实现。编程
在多数状况下,sync.Once 被用于控制变量的初始化,这个变量的读写一般遵循单例模式,知足这三个条件:网络
在 net 库里,系统的网络配置就是存放在一个变量里,代码以下:函数
`package net` `var (` `// guards init of confVal via initConfVal` `confOnce sync.Once` `confVal = &conf{goos: runtime.GOOS}` `)` `// systemConf returns the machine's network configuration.` `func systemConf() *conf {` `confOnce.Do(initConfVal)` `return confVal` `}` `func initConfVal() {` `dnsMode, debugLevel := goDebugNetDNS()` `confVal.dnsDebugLevel = debugLevel` `// 省略部分代码...` `}`
上面这段代码里,confVal
存放数据, confOnce
控制读写,两个都是 package-level 单例变量。因为 Go 里变量被初始化为默认值,confOnce
能够被当即使用,咱们重点关注confOnce.Do
。首先当作员函数 Do
的定义:工具
func (o *Once) Do(f func())
Do
接收一个函数做为参数,该函数不接受任务参数,不返回任何参数。具体作什么由使用方决定,错误处理也由使用方控制。 ui
once.Sync
可用于任何符合 “exactly once” 语义的场景,好比:atom
Go语言中,文件被重复关闭会报error,而 channel 被重复关闭报 panic,once.Sync
能够保证这类事情不发生,可是不能保证其余业务层面的错误。下面这个例子给出了一种错误处理的方式,供你们参考:
`// source: os/exec/exec.go` `package exec` `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()` `}`
sync.Once 类经过一个锁变量和原子变量保障 exactly once
语义,直接撸下源码(为了便于阅读,作了简化处理):
`package sync` `import "sync/atomic"` `type Once struct {` `done uint32` `m Mutex` `}` `func (o *Once) Do(f func()) {` `if atomic.LoadUint32(&o.done) == 0 {` `o.m.Lock()` `defer o.m.Unlock()` `if o.done == 0 {` `defer atomic.StoreUint32(&o.done, 1)` `f()` `}` `}` `}`
这里 done
是一个状态位,用于判断变量是否初始化完成,其有效值是:
done
默认值就是0f
不会被再次执行而 m Mutex
用于控制临界区的进入,保证同一时间点最多有一个 f
在执行。
done
在 m.Lock()
先后的两次校验都是必要的。
在 Scala 里,有一个关键词 lazy
,实现了 sync.Once 一样的功能。具体实现上,早期版本使用了 volatile 修饰状态变量 done
,使用 synchronized
替代 m Mutex
;后来,也改为了基于CAS的方式。
使用体验上,显然 lazy
更香!