Go 并发编程实战课
晁岳攀(鸟窝)
前微博技术专家,知名微服务框架 rpcx 作者
25635 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 22 讲
Go 并发编程实战课
15
15
1.0x
00:00/00:00
登录|注册

08 | Once:一个简约而不简单的并发原语

Once在第一次使用之后的复制问题
slowXXXX方法的好处
初始化失败后调用Do方法再次尝试初始化
避免在f参数中调用当前的Once
初始化测试资源
初始化Cache资源
只有第一次调用Do方法时f参数才会执行
思考题
Once的广泛应用场景
使用Reset方法时的注意事项
Reset方法导致的问题
未初始化
死锁
自定义的Once并发原语
双检查机制
使用互斥锁保证线程安全
标准库内部实现中的应用场景
闭包方式引用外部参数
sync.Once的Do方法
在测试的时候初始化一次测试资源
并发访问只需初始化一次的共享资源
初始化单例资源
总结
Once的踩坑案例
错误使用场景
实现方法
使用方法
使用场景
Once并发原语

该思维导图由 AI 生成,仅供参考

你好,我是鸟窝。
这一讲我来讲一个简单的并发原语:Once。为什么要学习 Once 呢?我先给你答案:Once 可以用来执行且仅仅执行一次动作,常常用于单例对象的初始化场景。
那这节课,我们就从对单例对象进行初始化这件事儿说起。
初始化单例资源有很多方法,比如定义 package 级别的变量,这样程序在启动的时候就可以初始化:
package abc
import time
var startTime = time.Now()
或者在 init 函数中进行初始化:
package abc
var startTime time.Time
func init() {
startTime = time.Now()
}
又或者在 main 函数开始执行的时候,执行一个初始化的函数:
package abc
var startTime time.Tim
func initApp() {
startTime = time.Now()
}
func main() {
initApp()
}
这三种方法都是线程安全的,并且后两种方法还可以根据传入的参数实现定制化的初始化操作。
但是很多时候我们是要延迟进行初始化的,所以有时候单例资源的初始化,我们会使用下面的方法:
package main
import (
"net"
"sync"
"time"
)
// 使用互斥锁保证线程(goroutine)安全
var connMu sync.Mutex
var conn net.Conn
func getConn() net.Conn {
connMu.Lock()
defer connMu.Unlock()
// 返回已创建好的连接
if conn != nil {
return conn
}
// 创建连接
conn, _ = net.DialTimeout("tcp", "baidu.com:80", 10*time.Second)
return conn
}
// 使用连接
func main() {
conn := getConn()
if conn == nil {
panic("conn is nil")
}
}
这种方式虽然实现起来简单,但是有性能问题。一旦连接创建好,每次请求的时候还是得竞争锁才能读取到这个连接,这是比较浪费资源的,因为连接如果创建好之后,其实就不需要锁的保护了。怎么办呢?
这个时候就可以使用这一讲要介绍的 Once 并发原语了。接下来我会详细介绍 Once 的使用、实现和易错场景。

Once 的使用场景

sync.Once 只暴露了一个方法 Do,你可以多次调用 Do 方法,但是只有第一次调用 Do 方法时 f 参数才会执行,这里的 f 是一个无参数无返回值的函数。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文介绍了并发编程中的一个重要问题,即如何保证某个动作只执行一次。作者首先介绍了单例对象的初始化方法,然后引出了使用互斥锁保证线程安全的方式,但指出了性能问题。接着详细介绍了并发原语Once的使用场景和实现方式,包括在标准库内部的应用。作者还提供了Once的实现方式,指出了正确的实现方式需要使用互斥锁和双检查机制,以避免并发情况下的问题。最后,作者提到了使用Once可能出现的两种错误场景,包括死锁和递归调用。通过清晰的示例和详细的讲解,读者能够快速了解Once的使用方法和实现原理,为并发编程提供了有益的参考。文章还介绍了一个功能更加强大的Once的实现方式,以及一些使用Once可能出现的错误场景和解决方法。总而言之,Once的应用场景还是很广泛的,一旦读者遇到只需要初始化一次的场景,首先想到的就应该是Once并发原语。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Go 并发编程实战课》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(27)

  • 最新
  • 精选
  • 端贺
    问题一:分离固定内容和非固定内容,使得固定的内容能被内联调用,从而优化执行过程。 问题二:Once被拷贝的过程中内部的已执行状态不会改变,所以Once不能通过拷贝多次执行。 不知道回答对不对,请老师指点。

    作者回复: 对

    2020-11-12
    3
    17
  • 大力
    请教这里所说的内联,提高执行效率是什么意思?

    作者回复: 你可以搜一下golang inline,有几位大牛已经介绍了内联优化的知识

    2020-11-16
    2
    6
  • 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-29
    13
    6
  • 🍀柠檬鱼也是鱼
    once为什么不直接加锁,还需要加多一个 双重检测呢?这块不太懂,望老师解答,我的理解是,调用do()之后直接上锁,等执行完f()再解锁不就行了吗

    作者回复: 因为有并发初始化的问题

    2020-10-28
    4
    6
  • Linuxer
    第一个思考题:Linux内核也有很多这种fast code path和slow code path,我想这样划分是不是内聚性更好,实现更清晰呢,从linux性能分析来看,貌似更多关注点是在slow code path 第二个思考题:应该不可以吧,Once的内部状态已经被改变了

    作者回复: fast path的一个好处是此方法可以内联

    2020-10-28
    4
  • 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
收起评论
显示
设置
留言
27
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部