关于golang:Golang-package-sync-剖析一-syncOnce

52次阅读

共计 2670 个字符,预计需要花费 7 分钟才能阅读完成。

前言

Go 语言在设计上对同步(Synchronization,数据同步和线程同步)提供大量的反对,比方 goroutine 和 channel 同步原语,库层面有

  • sync:提供根本的同步原语(比方 Mutex、RWMutex、Locker)和 工具类(Once、WaitGroup、Cond、Pool、Map)
  • sync/atomic:提供原子操作(基于硬件指令 compare-and-swap)

留神:__当我说“类”时,是指 Go 里的 struct(__独身狗要有面向“对象”编程的觉醒__)。

Go 语言里对同步的反对次要有五类利用场景:

  1. 资源独占:当多个线程依赖同一份资源(比方数据),须要同时读 / 写同一个内存地址时,runtime 须要保障只有一个批改这份数据,并且保障该批改对其余线程可见。锁和变量的原子操作为此而设计;
  2. 生产者 - 消费者:在生产者 - 消费者模型中,消费者依赖生产者产出数据。channel(管道)为此而设计;
  3. 懒加载:一个资源,当且仅当第一次执行一个操作,该操作执行过程中其余的同类操作都会被阻塞,直到该操作实现。sync.Once 为此而设计;
  4. fork-join:一个工作首先创立出 N 个子工作,N 个子工作全副执行实现当前,主工作收集后果,执行后续操作。sync.WaitGroup 为此而设计;
  5. 条件变量:条件变量是一个同步原语,能够同时阻塞多个线程,直到另一个线程 1) 批改了条件; 2) 告诉一个(或所有)期待的线程。sync.Cond 为此而设计;

留神:__这里当我说”线程”时,理解 Go 的同学能够主动映射到“goroutine”(协程)。

对于 1 和 2,通过官网文档理解其用法和实现。本系列的配角是 sync 下的工工具类,从 sync.Once 开始。内容分两局部:sync.Once 用法和 sync.Once 实现。

sync.Once 用法

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

  1. 当且仅当第一次读某个变量时,进行初始化(写操作)
  2. 变量被初始化过程中,所有读都被阻塞(读操作;当变量初始化实现后,读操作持续进行
  3. 变量仅初始化一次,初始化实现后驻留在内存里

在 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”语义的场景,比方:

  1. 初始化 rpc/http client
  2. open/close 文件
  3. close channel
  4. 线程池初始化

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 在执行。

donem.Lock() 前后的两次校验都是必要的。

发散一下

在 Scala 里,有一个关键词 lazy,实现了 sync.Once 同样的性能。具体实现上,晚期版本应用了 volatile 润饰状态变量 done,应用 synchronized 代替 m Mutex;起初,也改成了基于 CAS 的形式。

应用体验上,显然 lazy 更香!

References

  1. Golang: sync.Once https://golang.org/pkg/sync/#…

  2. Synchronization(Computer Science) https://en.wikipedia.org/wiki…\_(computer\_science)

  3. SIP-20 – Improved Lazy Vals Initialization http://scalajp.github.io/scal…

正文完
 0