原文连接: https://blog.thinkeridea.com/202101/go/exsync/once.htmlhtml
官方描述 Once is an object that will perform exactly one action
, 即 Once
是一个对象,它提供了保证某个动做只被执行一次功能,最典型的场景就是单例模式,Once
可用于任何符合 "exactly once" 语义的场景。git
在多数状况下,sync.Once
被用于控制变量的初始化,这个变量的读写一般遵循单例模式,知足这三个条件:github
<!--more-->算法
在标准库中不乏有大量 sync.Once
的使用案例,在 strings
包中 replace.go
里实现字符串批量替换功能时,须要预编译生成替换规则,即采用不一样的替换算法并建立相关算法实例,因 strings.Replacer
实现是线程安全且支持规则复用,在第一次解析替换规则并建立对应算法实例后,能够并发的进行字符串替换操做,避免屡次解析替换规则浪费资源。数据库
先看一下 strings.Replacer
的结构定义:安全
// 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.Replace
和 Replacer.WriteString
)进行的, r.once.Do(r.buildOnce)
使用 sync.Once
的 Do
方法保证只有在首次执行时才会执行 buildOnce
方法,而在 buildOnce
中调用 build
解析替换规则并建立对应算法实例,在 buildOnce
中进行赋值。并发
// 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
才能获得返回。 app
once.Do
接收一个函数做为参数,该函数不接受任何参数,不返回任何参数。具体作什么由使用方决定,错误处理也由使用方控制,对函数初始化的结果也由使用方进行保存。ide
这给出了一种错误处理的例子 exec.closeOnce
,exec.closeOnce
保证了重复关闭文件,永远只执行一次,而且老是返回首次关闭产生的错误信息:函数
// 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() }
Once
的实现很是的灵活、简洁、高效,排除注释部分 Once
仅用 17 行实现,且单次执行时间在 0.3ns 左右。这让我十分敬佩,对它可谓喜好至极,但由于它的通用性,在使用 Once
时给我带来了一些小小的负担,这也成了我极少的使用它的缘由。
Once
只保证调用安全性(即线程安全以及只执行一次动做函数),可是细心的朋友必定发现了咱们每每须要配对定义 Once
和业务实例变量,极少使用的状况下(如上述两个例子)看起来并无什么负担,可是若是咱们项目中有大量实例进行管理时(通常是集中管理,便于解决依赖问题),这时就会变得有点丑陋。
一个实际的业务场景,我有一个 http
服务,它有数百个组件实例,咱们建立了一个 APP
用来管理全部实例的初始化、依赖关系,从而保证各个组件依赖其接口,相互之间进行解耦,也使得每一个组件的配置(初始化参数)、依赖易于管理,不过咱们经常对单例实例在 http
服务启动时进行初始化,这样避免使用 Once
,且能够在 http
服务启动时暴露外部依赖问题(数据库、其它服务等)。
这个 http
服务须要不少辅助命令,每一个命令负责极少的工做,若是我在命令启动时使用 APP
初始化全部组件,这形成了大量的资源浪费。我单独实现一个 Command
依赖管理组件,它大量使用 Once
保证各个组件只在第一次使用时进行初始化,这给我带来了一些困扰,我大量定义 Once
的实例,且它和具体的组件实例没有关联,我在使用时须要很是的当心。
使用过 go-extend/pool 中的 pool.BufferPool 的朋友若是留意其源码的话会发现其中定义了一些 sync.Once
的实例,这相对上诉场景倒是相对少的,如下即是 pool.BufferPool 中的部分代码:
// source: https://github.com/thinkeridea/go-extend/blob/v1.1.2/pool/buffer.go package pool import ( "bytes" "sync" ) var ( buff64 *sync.Pool buff128 *sync.Pool buff512 *sync.Pool buff1024 *sync.Pool buff2048 *sync.Pool buff4096 *sync.Pool buff8192 *sync.Pool buff64One sync.Once buff128One sync.Once buff512One sync.Once buff1024One sync.Once buff2048One sync.Once buff4096One sync.Once buff8192One sync.Once ) type pool sync.Pool // BufferPool bytes.Buffer 的 sync.Pool 接口 // 能够直接 Get *bytes.Buffer 并 Reset Buffer type BufferPool interface { // Get 从 Pool 中获取一个 *bytes.Buffer 实例, 该实例已经被 Reset Get() *bytes.Buffer // Put 把 *bytes.Buffer 放回 Pool 中 Put(*bytes.Buffer) } func newBufferPool(size int) *sync.Pool { return &sync.Pool{ New: func() interface{} { return bytes.NewBuffer(make([]byte, size)) }, } } // GetBuff64 获取一个初始容量为 64 的 *bytes.Buffer Pool func GetBuff64() BufferPool { buff64One.Do(func() { buff64 = newBufferPool(64) }) return (*pool)(buff64) }
上诉代码中定义了 buff64One
到 buff8192One
7个 Once
的实例,且对应的存在 buff64
到 buff8192
的业务实例,我在 GetBuff64
中必须当心使用 Once
实例,避免错误使用致使对应的实例未被初始化,并且上诉的代码看起来还有一些丑陋。
鉴于我对 sync.Once
灵活、简洁、高效的喜好,不能仅仅由于它的“吝啬”(极简的功能)便与之诀别,促使我开启了探寻缓和与 sync.Once
关系之路。
首先我想到的是对 sync.Once
的二次包装,使其能够保存一个数据,这样我就能够只定义 Once
的实例,由 Once
负责存储初始化的结果。exsync.Once 这是个人第一个实验,它的实现很是简洁:
// source: https://github.com/thinkeridea/go-extend/blob/efa13c9456cb4ce97c16824de2996c84fa285fc3/exsync/once.go type Once struct { once sync.Once v interface{} } func (o *Once) Do(f func() interface{}) interface{} { o.once.Do(func() { o.v = f() }) return o.v }
它嵌套一个 sync.Once
实例,并覆盖其 Do
函数,使其接收一个 func() interface{}
函数,它要求初始化函数返回其结果,结果保存在 Once.v
,每次调用 Do
它便返回本身保存的结果,这使用起来就变得简单许多,改造以前 exec.closeOnce
例子:
type closeOnce struct { *os.File once exsync.Once } func (c *closeOnce) Close() error { return c.once.Do(c.close).(error) } func (c *closeOnce) close() interface{} { return c.File.Close() }
这减小了一个业务层的数据定义,若是包含多个数据,可使用自定义 struct
或者 []interface{}
进行数据保存, 一个简单打开文件的例子:
type openOnce struct { file exsync.Once } func (c *openOnce) Open(name string) (*os.File, error) { f := c.file.Do(func() interface{} { f, err := os.Open(name) return []interface{}{f, err} }).([]interface{}) return f[0].(*os.File), f[1].(error) }
这看起来使初始化的代码变得复杂了一些,对多返回值的问题暂时没有更好的实现,我会在后续逐渐考虑这类问题的处理方式,单个值时它使我获得一些惊喜和便捷。即便这样我随后发现它相对 sync.Once
的性能大幅度降低,达到10倍之多,起初我认为是 interface
的带来的,我马上实现了一个 exsync.OncePointer 以期许它能够在性能上给我一个惊喜:
// source: https://github.com/thinkeridea/go-extend/blob/efa13c9456cb4ce97c16824de2996c84fa285fc3/exsync/once.go type OncePointer struct { once sync.Once v unsafe.Pointer } func (o *OncePointer) Do(f func() unsafe.Pointer) unsafe.Pointer { o.once.Do(func() { o.v = f() }) return o.v }
使用 unsafe.Pointer
存储实例,让其在编译时肯定类型,来提高其性能,使用示例以下:
type closeOnce struct { *os.File once exsync.OncePointer } func (c *closeOnce) Close() error { return *(*error)(c.once.Do(c.close)) } func (c *closeOnce) close() unsafe.Pointer { err := c.File.Close() return unsafe.Pointer(&err) }
尴尬的是这并无使其性能有极大提高,仅仅只是稍微提高一些,难道我要和 sync.Once
就此诀别,仍是凑合过……
我本已放弃优化,即便其性能极大降低,可是它仍然能够在 3ns 内完成任务,这并不会造成瓶颈。但多少心里仍是有些不甘,仅仅只是包装使其保存一个值不该该致使性能降低如此严重,到底是什么致使其性能如此严重降低的,仔细作了分析发现因为 sync.Once
很是的高效,且代码简洁,我嵌套包装使其多了一层调用,且可能致使其没法内联,这对一些性能不高的组件影响极小,可是像 sync.Once
这样高效任何小小的损耗表现都十分明显。
我直接拷贝 sync.Once
中的代码到 exsync.Once 及 exsync.OncePointer 实现中,这让我获得与 sync.Once
接近的性能,exsync.OncePointer 的实现甚至老是好于 sync.Once
。
如下是性能测试的结果,其代码位于 exsync/benchmark/once_test.go:
goos: darwin goarch: amd64 pkg: github.com/thinkeridea/go-extend/exsync/benchmark BenchmarkSyncOnce-8 1000000000 0.391 ns/op 0 B/op 0 allocs/op BenchmarkOnce-8 1000000000 0.407 ns/op 0 B/op 0 allocs/op BenchmarkOncePointer-8 1000000000 0.389 ns/op 0 B/op 0 allocs/op PASS ok github.com/thinkeridea/go-extend/exsync/benchmark 1.438s
获得这个结果后我坚决果断、快马加鞭的改变了 pool.BufferPool 中的代码,这使 pool.BufferPool 变得简洁许多:
package pool import ( "bytes" "sync" "unsafe" "github.com/thinkeridea/go-extend/exsync" ) var ( buff64 exsync.OncePointer buff128 exsync.OncePointer buff512 exsync.OncePointer buff1024 exsync.OncePointer buff2048 exsync.OncePointer buff4096 exsync.OncePointer buff8192 exsync.OncePointer ) type bufferPool struct { sync.Pool } // BufferPool bytes.Buffer 的 sync.Pool 接口 // 能够直接 Get *bytes.Buffer 并 Reset Buffer type BufferPool interface { // Get 从 Pool 中获取一个 *bytes.Buffer 实例, 该实例已经被 Reset Get() *bytes.Buffer // Put 把 *bytes.Buffer 放回 Pool 中 Put(*bytes.Buffer) } func newBufferPool(size int) unsafe.Pointer { return unsafe.Pointer(&bufferPool{ Pool: sync.Pool{ New: func() interface{} { return bytes.NewBuffer(make([]byte, size)) }, }, }) } // GetBuff64 获取一个初始容量为 64 的 *bytes.Buffer Pool func GetBuff64() BufferPool { return (*bufferPool)(buff64.Do(func() unsafe.Pointer { return newBufferPool(64) })) }
如此对 sync.Once
进行二次封装,使其通用性有所降低,并必定是一个好的方案,我乐于公开它,由于它在大多数时刻能够减小使用者的负担,使得代码变的简练。
后续的思考:
Once
永远只能执行一次,是否有安全快捷的方法可使其重置。Do
函数。解决以上这些问题,可使 sync.Once
应用在更多的场景中,但势必致使其性能有所降低,这须要一些实验和折中处理。
转载:
本文做者: 戚银(thinkeridea)
本文连接: https://blog.thinkeridea.com/202101/go/exsync/once.html
版权声明: 本博客全部文章除特别声明外,均采用 CC BY 4.0 CN协议 许可协议。转载请注明出处!