一.序

单从库名大概就能猜出其作用。sync.Once使用起来很简单, 下面是一个简单的使用案例

package main import (“fmt””sync”) func main() {var (once sync.Oncewg sync.WaitGroup) for i := 0; i < 10; i++ {wg.Add(1)// 这里要注意讲i显示的当参数传入内部的匿名函数go func(i int) {defer wg.Done()// fmt.Println(“once”, i)once.Do(func() {fmt.Println(“once”, i)})}(i)} wg.Wait()fmt.Printf(“over”)}

输出:

go run ./demo.go once 9

测试如果不添加once.Do 这段代码,则会输出如下结果,并且每次执行的输出都不一样。

once 9 once 0 once 3 once 6 once 4 once 1 once 5 once 2 once 7 once 8

从两次输出不同,我们可以得知 sync.Once的作用是:保证传入的函数只执行一次

二. 源码分析 2.1结构体

Once的结构体如下

type Once struct { done uint32 m Mutex}

每一个 sync.Once 结构体中都只包含一个用于标识代码块是否执行过的 done 以及一个互斥锁 sync.Mutex

2.2 接口

sync.Once.Do 是 sync.Once 结构体对外唯一暴露的方法,该方法会接收一个入参为空的函数:

如果传入的函数已经执行过,会直接返回 如果传入的函数没有执行过, 会调用sync.Once.doSlow执行传入的参数 func (o *Once) Do(f func()) {// Note: Here is an incorrect implementation of Do:////if atomic.CompareAndSwapUint32(&o.done, 0, 1) {//f()//}//// Do guarantees that when it returns, f has finished.// This implementation would not implement that guarantee:// given two simultaneous calls, the winner of the cas would// call f, and the second would return immediately, without// waiting for the first’s call to f to complete.// This is why the slow path falls back to a mutex, and why// the atomic.StoreUint32 must be delayed until after f returns. if atomic.LoadUint32(&o.done) == 0 {// Outlined slow-path to allow inlining of the fast-path.o.doSlow(f)}}

代码注释中特别给了一个说明: 很容易犯错的一种实现

if atomic.CompareAndSwapUint32(&o.done, 0, 1) {f()}

如果这么实现最大的问题是,如果并发调用,一个 goroutine 执行,另外一个不会等正在执行的这个成功之后返回,而是直接就返回了,这就不能保证传入的方法一定会先执行一次了

正确的实现方式

if atomic.LoadUint32(&o.done) == 0 { // Outlined slow-path to allow inlining of the fast-path. o.doSlow(f)}

会先判断 done 是否为 0,如果不为 0 说明还没执行过,就进入 doSlow

func (o *Once) doSlow(f func()) {o.m.Lock()defer o.m.Unlock()if o.done == 0 {defer atomic.StoreUint32(&o.done, 1)f()}}

在 doSlow 当中使用了互斥锁来保证只会执行一次

具体的逻辑

为当前Goroutine获取互斥锁 执行传入的无入参函数; 运行延迟函数, 将成员变量done更新为1 三. 使用场景案例 3.1 单例模式

原子操作配合互斥锁可以实现非常高效的单件模式。互斥锁的代价比普通整数的原子读写高很多,在性能敏感的地方可以增加一个数字型的标志位,通过原子检测标志位状态降低互斥锁的使用次数来提高性能。

type singleton struct {} var ( instance *singleton initialized uint32 mu sync.Mutex) func Instance() *singleton { if atomic.LoadUint32(&initialized) == 1 { return instance } mu.Lock() defer mu.Unlock() if instance == nil { defer atomic.StoreUint32(&initialized, 1) instance = &singleton{} } return instance}

而使用sync.Once能更简单实现单例模式

type singleton struct {} var ( instance *singleton once sync.Once) func Instance() *singleton { once.Do(func() { instance = &singleton{} }) return instance} 3.2 加载配置文件示例

延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。我们来看一个例子:

var icons map[string]image.Image func loadIcons() { icons = map[string]image.Image{ “left”: loadIcon(“left.png”), “up”: loadIcon(“up.png”), “right”: loadIcon(“right.png”), “down”: loadIcon(“down.png”), }} // Icon 被多个goroutine调用时不是并发安全的// 因为map类型本就不是类型安全数据结构func Icon(name string) image.Image { if icons == nil { loadIcons() } return icons[name]}

多个goroutine并发调用Icon函数时不是并发安全的,编译器和CPU可能会在保证每个goroutine都满足串行一致的基础上自由地重排访问内存的顺序。loadIcons函数可能会被重排为以下结果:

func loadIcons() {     icons = make(map[string]image.Image)     icons[“left”] = loadIcon(“left.png”)     icons[“up”] = loadIcon(“up.png”)     icons[“right”] = loadIcon(“right.png”)     icons[“down”] = loadIcon(“down.png”) }

在这种情况下就会出现即使判断了icons不是nil也不意味着变量初始化完成了。考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化icons的时候不会被其他的goroutine操作,但是这样做又会引发性能问题。

可以使用sync.Once 改造代码

var icons map[string]image.Image var loadIconsOnce sync.Once func loadIcons() { icons = map[string]image.Image{ “left”: loadIcon(“left.png”), “up”: loadIcon(“up.png”), “right”: loadIcon(“right.png”), “down”: loadIcon(“down.png”), }} // Icon 是并发安全的,并且保证了在代码运行的时候才会加载配置func Icon(name string) image.Image { loadIconsOnce.Do(loadIcons) return icons[name]}

这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。

四.总结

作为用于保证函数执行次数的 sync.Once 结构体,它使用互斥锁和 sync/atomic 包提供的方法实现了某个函数在程序运行期间只能执行一次的语义。在使用该结构体时,我们也需要注意以下的问题:

sync.Once.Do 方法中传入的函数只会被执行一次,哪怕函数中发生了 panic; 两次调用 sync.Once.Do 方法传入不同的函数只会执行第一次调传入的函数; 五. 参考 https://lailin.xyz/post/go-training-week3-once.html https://www.topgoer.cn/docs/gozhuanjia/chapter055.2-waitgroup https://www.topgoer.com/并发编程/sync.html https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-05-mem.html

到此这篇关于Go

一.序

单从库名大概就能猜出其作用。sync.Once使用起来很简单, 下面是一个简单的使用案例

package main import (“fmt””sync”) func main() {var (once sync.Oncewg sync.WaitGroup) for i := 0; i < 10; i++ {wg.Add(1)// 这里要注意讲i显示的当参数传入内部的匿名函数go func(i int) {defer wg.Done()// fmt.Println(“once”, i)once.Do(func() {fmt.Println(“once”, i)})}(i)} wg.Wait()fmt.Printf(“over”)}

输出:

go run ./demo.go once 9

测试如果不添加once.Do 这段代码,则会输出如下结果,并且每次执行的输出都不一样。

once 9 once 0 once 3 once 6 once 4 once 1 once 5 once 2 once 7 once 8

从两次输出不同,我们可以得知 sync.Once的作用是:保证传入的函数只执行一次

二. 源码分析 2.1结构体

Once的结构体如下

type Once struct { done uint32 m Mutex}

每一个 sync.Once 结构体中都只包含一个用于标识代码块是否执行过的 done 以及一个互斥锁 sync.Mutex

2.2 接口

sync.Once.Do 是 sync.Once 结构体对外唯一暴露的方法,该方法会接收一个入参为空的函数:

如果传入的函数已经执行过,会直接返回 如果传入的函数没有执行过, 会调用sync.Once.doSlow执行传入的参数 func (o *Once) Do(f func()) {// Note: Here is an incorrect implementation of Do:////if atomic.CompareAndSwapUint32(&o.done, 0, 1) {//f()//}//// Do guarantees that when it returns, f has finished.// This implementation would not implement that guarantee:// given two simultaneous calls, the winner of the cas would// call f, and the second would return immediately, without// waiting for the first’s call to f to complete.// This is why the slow path falls back to a mutex, and why// the atomic.StoreUint32 must be delayed until after f returns. if atomic.LoadUint32(&o.done) == 0 {// Outlined slow-path to allow inlining of the fast-path.o.doSlow(f)}}

代码注释中特别给了一个说明: 很容易犯错的一种实现

if atomic.CompareAndSwapUint32(&o.done, 0, 1) {f()}

如果这么实现最大的问题是,如果并发调用,一个 goroutine 执行,另外一个不会等正在执行的这个成功之后返回,而是直接就返回了,这就不能保证传入的方法一定会先执行一次了

正确的实现方式

if atomic.LoadUint32(&o.done) == 0 { // Outlined slow-path to allow inlining of the fast-path. o.doSlow(f)}

会先判断 done 是否为 0,如果不为 0 说明还没执行过,就进入 doSlow

func (o *Once) doSlow(f func()) {o.m.Lock()defer o.m.Unlock()if o.done == 0 {defer atomic.StoreUint32(&o.done, 1)f()}}

在 doSlow 当中使用了互斥锁来保证只会执行一次

具体的逻辑

为当前Goroutine获取互斥锁 执行传入的无入参函数; 运行延迟函数, 将成员变量done更新为1 三. 使用场景案例 3.1 单例模式

原子操作配合互斥锁可以实现非常高效的单件模式。互斥锁的代价比普通整数的原子读写高很多,在性能敏感的地方可以增加一个数字型的标志位,通过原子检测标志位状态降低互斥锁的使用次数来提高性能。

type singleton struct {} var ( instance *singleton initialized uint32 mu sync.Mutex) func Instance() *singleton { if atomic.LoadUint32(&initialized) == 1 { return instance } mu.Lock() defer mu.Unlock() if instance == nil { defer atomic.StoreUint32(&initialized, 1) instance = &singleton{} } return instance}

而使用sync.Once能更简单实现单例模式

type singleton struct {} var ( instance *singleton once sync.Once) func Instance() *singleton { once.Do(func() { instance = &singleton{} }) return instance} 3.2 加载配置文件示例

延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。我们来看一个例子:

var icons map[string]image.Image func loadIcons() { icons = map[string]image.Image{ “left”: loadIcon(“left.png”), “up”: loadIcon(“up.png”), “right”: loadIcon(“right.png”), “down”: loadIcon(“down.png”), }} // Icon 被多个goroutine调用时不是并发安全的// 因为map类型本就不是类型安全数据结构func Icon(name string) image.Image { if icons == nil { loadIcons() } return icons[name]}

多个goroutine并发调用Icon函数时不是并发安全的,编译器和CPU可能会在保证每个goroutine都满足串行一致的基础上自由地重排访问内存的顺序。loadIcons函数可能会被重排为以下结果:

func loadIcons() {     icons = make(map[string]image.Image)     icons[“left”] = loadIcon(“left.png”)     icons[“up”] = loadIcon(“up.png”)     icons[“right”] = loadIcon(“right.png”)     icons[“down”] = loadIcon(“down.png”) }

在这种情况下就会出现即使判断了icons不是nil也不意味着变量初始化完成了。考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化icons的时候不会被其他的goroutine操作,但是这样做又会引发性能问题。

可以使用sync.Once 改造代码

var icons map[string]image.Image var loadIconsOnce sync.Once func loadIcons() { icons = map[string]image.Image{ “left”: loadIcon(“left.png”), “up”: loadIcon(“up.png”), “right”: loadIcon(“right.png”), “down”: loadIcon(“down.png”), }} // Icon 是并发安全的,并且保证了在代码运行的时候才会加载配置func Icon(name string) image.Image { loadIconsOnce.Do(loadIcons) return icons[name]}

这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。

四.总结

作为用于保证函数执行次数的 sync.Once 结构体,它使用互斥锁和 sync/atomic 包提供的方法实现了某个函数在程序运行期间只能执行一次的语义。在使用该结构体时,我们也需要注意以下的问题:

sync.Once.Do 方法中传入的函数只会被执行一次,哪怕函数中发生了 panic; 两次调用 sync.Once.Do 方法传入不同的函数只会执行第一次调传入的函数; 五. 参考 https://lailin.xyz/post/go-training-week3-once.html https://www.topgoer.cn/docs/gozhuanjia/chapter055.2-waitgroup https://www.topgoer.com/并发编程/sync.html https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-05-mem.html

到此这篇关于GoGo语言

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。