Go并发编程(八) 深入理解 sync.Once

楚江王
• 阅读 858

在上一篇文章《Week03: Go 并发编程(七) 深入理解 errgroup》当中看 errgourp  源码的时候我们发现最后返回 err  是通过 once 来只保证返回一个非 nil 的值的,本文就来看一下 Once 的使用与实现

案例

once 的使用很简单

`func main() {`
 `var (`
 `o  sync.Once`
 `wg sync.WaitGroup`
 `)`
 `for i := 0; i < 10; i++ {`
 `wg.Add(1)`
 `go func(i int) {`
 `defer wg.Done()`
 `o.Do(func() {`
 `fmt.Println("once", i)`
 `})`
 `}(i)`
 `}`
 `wg.Wait()`
`}`

输出

`❯ go run ./main.go`
`once 9`

源码分析

`type Once struct {`
 `done uint32`
 `m    Mutex`
`}`

done 用于判定函数是否执行,如果不为 0 会直接返回

`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)`
 `}`
`}`

看 go 的源码真的可以学到很多东西,在这里还给出了很容易犯错的一种实现

`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  当中使用了互斥锁来保证只会执行一次

总结

  • Once 保证了传入的函数只会执行一次,这常用在单例模式,配置文件加载,初始化这些场景下
  • 但是需要注意。Once 是不能复用的,只要执行过了,再传入其他的方法也不会再执行了
  • 并且 Once.Do 在执行的过程中如果 f 出现 panic,后面也不会再执行了

参考文献

  1. https://pkg.go.dev/sync#Once
  2. 6.2 同步原语与锁

Go并发编程(八) 深入理解 sync.Once

mohuishou

lailin.xyz 的博客账号,关注但不限于 Go 开发,云原生,K8s等

36篇原创内容

公众号

👆 关注我,_一起在知识的海洋遨游_

转发,点赞,在看就是对我最大的鼓励

Go并发编程(八) 深入理解 sync.Once

点击“阅读原文”查看参考文献等信息

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Easter79 Easter79
3年前
sync.Once
今天阅读go部分源码的时候发现了一个包sync.Once那么这个包来干什么的呢?通过百度和查看源码得知sync.Once可以控制函数只能被调用一次。不能多次重复调用。varconfOncesync.OnceconfOnce.Do(func(){log.Println("test")})
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql中时间比较的实现
MySql中时间比较的实现unix\_timestamp()unix\_timestamp函数可以接受一个参数,也可以不使用参数。它的返回值是一个无符号的整数。不使用参数,它返回自1970年1月1日0时0分0秒到现在所经过的秒数,如果使用参数,参数的类型为时间类型或者时间类型的字符串表示,则是从1970010100:00:0
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
MySQL数据库InnoDB存储引擎Log漫游(1)
作者:宋利兵来源:MySQL代码研究(mysqlcode)0、导读本文介绍了InnoDB引擎如何利用UndoLog和RedoLog来保证事务的原子性、持久性原理,以及InnoDB引擎实现UndoLog和RedoLog的基本思路。00–UndoLogUndoLog是为了实现事务的原子性,