08 | Once:一个简约而不简单的并发原语
晁岳攀
该思维导图由 AI 生成,仅供参考
你好,我是鸟窝。
这一讲我来讲一个简单的并发原语:Once。为什么要学习 Once 呢?我先给你答案:Once 可以用来执行且仅仅执行一次动作,常常用于单例对象的初始化场景。
那这节课,我们就从对单例对象进行初始化这件事儿说起。
初始化单例资源有很多方法,比如定义 package 级别的变量,这样程序在启动的时候就可以初始化:
或者在 init 函数中进行初始化:
又或者在 main 函数开始执行的时候,执行一个初始化的函数:
这三种方法都是线程安全的,并且后两种方法还可以根据传入的参数实现定制化的初始化操作。
但是很多时候我们是要延迟进行初始化的,所以有时候单例资源的初始化,我们会使用下面的方法:
这种方式虽然实现起来简单,但是有性能问题。一旦连接创建好,每次请求的时候还是得竞争锁才能读取到这个连接,这是比较浪费资源的,因为连接如果创建好之后,其实就不需要锁的保护了。怎么办呢?
这个时候就可以使用这一讲要介绍的 Once 并发原语了。接下来我会详细介绍 Once 的使用、实现和易错场景。
Once 的使用场景
sync.Once 只暴露了一个方法 Do,你可以多次调用 Do 方法,但是只有第一次调用 Do 方法时 f 参数才会执行,这里的 f 是一个无参数无返回值的函数。
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
本文介绍了并发编程中的一个重要问题,即如何保证某个动作只执行一次。作者首先介绍了单例对象的初始化方法,然后引出了使用互斥锁保证线程安全的方式,但指出了性能问题。接着详细介绍了并发原语Once的使用场景和实现方式,包括在标准库内部的应用。作者还提供了Once的实现方式,指出了正确的实现方式需要使用互斥锁和双检查机制,以避免并发情况下的问题。最后,作者提到了使用Once可能出现的两种错误场景,包括死锁和递归调用。通过清晰的示例和详细的讲解,读者能够快速了解Once的使用方法和实现原理,为并发编程提供了有益的参考。文章还介绍了一个功能更加强大的Once的实现方式,以及一些使用Once可能出现的错误场景和解决方法。总而言之,Once的应用场景还是很广泛的,一旦读者遇到只需要初始化一次的场景,首先想到的就应该是Once并发原语。
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Go 并发编程实战课》,新⼈⾸单¥59
《Go 并发编程实战课》,新⼈⾸单¥59
立即购买
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
登录 后留言
全部留言(27)
- 最新
- 精选
- 端贺问题一:分离固定内容和非固定内容,使得固定的内容能被内联调用,从而优化执行过程。 问题二:Once被拷贝的过程中内部的已执行状态不会改变,所以Once不能通过拷贝多次执行。 不知道回答对不对,请老师指点。
作者回复: 对
2020-11-12317 - 大力请教这里所说的内联,提高执行效率是什么意思?
作者回复: 你可以搜一下golang inline,有几位大牛已经介绍了内联优化的知识
2020-11-1626 - moooofly请教一个问题,在示例代码中有如下片段 ``` func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() // 双检查 if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() } } ``` 在双检查整改地方,读取 o.done 的值并没有使用使用 atomic.LoadUint32(&o.done) 的方式,按照我的理解,是因为已经处于 o.m.Lock() 的保护下的缘故;那是否 atomic.StoreUint32(&o.done, 1) 也可以直接 o.done = 1 呢?毕竟这个代码也在 o.m.Lock() 的保护下
作者回复: fast path中读取o.done,并没有在mutex保护之下
2020-10-29136 - 🍀柠檬鱼也是鱼once为什么不直接加锁,还需要加多一个 双重检测呢?这块不太懂,望老师解答,我的理解是,调用do()之后直接上锁,等执行完f()再解锁不就行了吗
作者回复: 因为有并发初始化的问题
2020-10-2846 - Linuxer第一个思考题:Linux内核也有很多这种fast code path和slow code path,我想这样划分是不是内聚性更好,实现更清晰呢,从linux性能分析来看,貌似更多关注点是在slow code path 第二个思考题:应该不可以吧,Once的内部状态已经被改变了
作者回复: fast path的一个好处是此方法可以内联
2020-10-284 - Geek_69bcfa// 值是3.0或者0.0的一个数据结构 var threeOnce struct { sync.Once v *Float } // 返回此数据结构的值,如果还没有初始化为3.0,则初始化 func three() *Float { threeOnce.Do(func() { // 使用Once初始化 threeOnce.v = NewFloat(3.0) }) return threeOnce.v } 这个代码里 NewFloat(3.0),那后面程序用完后怎么释放这个new出来的内存?是否建议在 程序退出前对这个做一下 delete 呢?
作者回复: 程序退出就释放,这是操作系统的事
2023-03-22归属地:湖南 - INFRA_1求分享第一题答案
作者回复: 看评论,第一位已经回答,方便内联,提升性能
2022-11-01归属地:北京 - i_chase还可以只内联函数在doSlow的前的部分吗,一直以为内联是将整个函数内联上去了
作者回复: 内联整个函数
2022-07-23 - xl666‘’这确实是一种实现方式,但是,这个实现有一个很大的问题,就是如果参数 f 执行很慢的话,后续调用 Do 方法的 goroutine 虽然看到 done 已经设置为执行过了,但是获取某些初始化资源的时候可能会得到空的资源,因为 f 还没有执行完。‘’老师我们运行Do初始化的时候 一般加锁保证线程安全 那就就是说 抢到锁的gofunc在初始化fn()没有运行结束时不会释放锁 其他gofunc进不来 所以不会导致上面说的那种情况 我是这样理解的 要是初始化不加锁倒是会
作者回复: 其它goroutine也在等待锁啊,等第一个释放锁后就进来了
2022-02-09 - Bynow请问:(*uint32)(unsafe.Pointer(&o.Once)) 这个表达式为什么会是1?
作者回复: 这是unsafe.Pointer常用方法,你可以找这个类型的教程深入了解下
2021-12-17
收起评论