共计 2670 个字符,预计需要花费 7 分钟才能阅读完成。
前言
Go 语言在设计上对同步(Synchronization,数据同步和线程同步)提供大量的反对,比方 goroutine 和 channel 同步原语,库层面有
- sync:提供根本的同步原语(比方 Mutex、RWMutex、Locker)和 工具类(Once、WaitGroup、Cond、Pool、Map)
- sync/atomic:提供原子操作(基于硬件指令 compare-and-swap)
留神:__当我说“类”时,是指 Go 里的 struct(__独身狗要有面向“对象”编程的觉醒__)。
Go 语言里对同步的反对次要有五类利用场景:
- 资源独占:当多个线程依赖同一份资源(比方数据),须要同时读 / 写同一个内存地址时,runtime 须要保障只有一个批改这份数据,并且保障该批改对其余线程可见。锁和变量的原子操作为此而设计;
- 生产者 - 消费者:在生产者 - 消费者模型中,消费者依赖生产者产出数据。channel(管道)为此而设计;
- 懒加载:一个资源,当且仅当第一次执行一个操作,该操作执行过程中其余的同类操作都会被阻塞,直到该操作实现。sync.Once 为此而设计;
- fork-join:一个工作首先创立出 N 个子工作,N 个子工作全副执行实现当前,主工作收集后果,执行后续操作。sync.WaitGroup 为此而设计;
- 条件变量:条件变量是一个同步原语,能够同时阻塞多个线程,直到另一个线程 1) 批改了条件; 2) 告诉一个(或所有)期待的线程。sync.Cond 为此而设计;
留神:__这里当我说”线程”时,理解 Go 的同学能够主动映射到“goroutine”(协程)。
对于 1 和 2,通过官网文档理解其用法和实现。本系列的配角是 sync 下的工工具类,从 sync.Once 开始。内容分两局部: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
接管一个函数作为参数,该函数不接受任务参数,不返回任何参数。具体做什么由应用方决定,错误处理也由应用方管制。
once.Sync
可用于任何合乎“exactly once”语义的场景,比方:
- 初始化 rpc/http client
- open/close 文件
- close channel
- 线程池初始化
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 实现
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
是一个状态位,用于判断变量是否初始化实现,其有效值是:
- 0: 函数 f 尚未执行或执行中,Once 对象创立时
done
默认值就是 0 - 1: 函数 f 曾经执行完结,保障
f
不会被再次执行
而 m Mutex
用于管制临界区的进入,保障同一时间点最多有一个 f
在执行。
done
在 m.Lock()
前后的两次校验都是必要的。
发散一下
在 Scala 里,有一个关键词 lazy
,实现了 sync.Once 同样的性能。具体实现上,晚期版本应用了 volatile 润饰状态变量 done
,应用 synchronized
代替 m Mutex
;起初,也改成了基于 CAS 的形式。
应用体验上,显然 lazy
更香!
References
-
Golang: sync.Once https://golang.org/pkg/sync/#…
-
Synchronization(Computer Science) https://en.wikipedia.org/wiki…\_(computer\_science)
- SIP-20 – Improved Lazy Vals Initialization http://scalajp.github.io/scal…