一.序

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

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. func main() {
  7. var (
  8. once sync.Once
  9. wg sync.WaitGroup
  10. )
  11. for i := 0; i < 10; i++ {
  12. wg.Add(1)
  13. // 这里要注意讲i显示的当参数传入内部的匿名函数
  14. go func(i int) {
  15. defer wg.Done()
  16. // fmt.Println("once", i)
  17. once.Do(func() {
  18. fmt.Println("once", i)
  19. })
  20. }(i)
  21. }
  22. wg.Wait()
  23. fmt.Printf("over")
  24. }

输出:

  1. go run ./demo.go
  2. once 9

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

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

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

二. 源码分析

2.1结构体

Once的结构体如下

  1. type Once struct {
  2. done uint32
  3. m Mutex
  4. }

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

2.2 接口

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

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

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

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

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

正确的实现方式

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

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

  1. func (o *Once) doSlow(f func()) {
  2. o.m.Lock()
  3. defer o.m.Unlock()
  4. if o.done == 0 {
  5. defer atomic.StoreUint32(&o.done, 1)
  6. f()
  7. }
  8. }

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

具体的逻辑

  1. 为当前Goroutine获取互斥锁
  2. 执行传入的无入参函数;
  3. 运行延迟函数, 将成员变量done更新为1

三. 使用场景案例

3.1 单例模式

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

  1. type singleton struct {}
  2. var (
  3. instance *singleton
  4. initialized uint32
  5. mu sync.Mutex
  6. )
  7. func Instance() *singleton {
  8. if atomic.LoadUint32(&initialized) == 1 {
  9. return instance
  10. }
  11. mu.Lock()
  12. defer mu.Unlock()
  13. if instance == nil {
  14. defer atomic.StoreUint32(&initialized, 1)
  15. instance = &singleton{}
  16. }
  17. return instance
  18. }

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

  1. type singleton struct {}
  2. var (
  3. instance *singleton
  4. once sync.Once
  5. )
  6. func Instance() *singleton {
  7. once.Do(func() {
  8. instance = &singleton{}
  9. })
  10. return instance
  11. }

3.2 加载配置文件示例

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

  1. var icons map[string]image.Image
  2. func loadIcons() {
  3. icons = map[string]image.Image{
  4. "left": loadIcon("left.png"),
  5. "up": loadIcon("up.png"),
  6. "right": loadIcon("right.png"),
  7. "down": loadIcon("down.png"),
  8. }
  9. }
  10. // Icon 被多个goroutine调用时不是并发安全的
  11. // 因为map类型本就不是类型安全数据结构
  12. func Icon(name string) image.Image {
  13. if icons == nil {
  14. loadIcons()
  15. }
  16. return icons[name]
  17. }

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

  1. func loadIcons() {
  2. icons = make(map[string]image.Image)
  3. icons["left"] = loadIcon("left.png")
  4. icons["up"] = loadIcon("up.png")
  5. icons["right"] = loadIcon("right.png")
  6. icons["down"] = loadIcon("down.png")
  7. }

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

可以使用sync.Once 改造代码

  1. var icons map[string]image.Image
  2. var loadIconsOnce sync.Once
  3. func loadIcons() {
  4. icons = map[string]image.Image{
  5. "left": loadIcon("left.png"),
  6. "up": loadIcon("up.png"),
  7. "right": loadIcon("right.png"),
  8. "down": loadIcon("down.png"),
  9. }
  10. }
  11. // Icon 是并发安全的,并且保证了在代码运行的时候才会加载配置
  12. func Icon(name string) image.Image {
  13. loadIconsOnce.Do(loadIcons)
  14. return icons[name]
  15. }

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

四.总结

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

  • sync.Once.Do 方法中传入的函数只会被执行一次,哪怕函数中发生了 panic;
  • 两次调用 sync.Once.Do 方法传入不同的函数只会执行第一次调传入的函数;

五. 参考

  1. https://lailin.xyz/post/go-training-week3-once.html
  2. https://www.topgoer.cn/docs/gozhuanjia/chapter055.2-waitgroup
  3. https://www.topgoer.com/并发编程/sync.html
  4. https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-05-mem.html

七. Go并发编程--sync.Once的更多相关文章

  1. 十二. Go并发编程--sync/errGroup

    一.序 这一篇算是并发编程的一个补充,起因是当前有个项目,大概の 需求是,根据kafka的分区(partition)数,创建同等数量的 消费者( goroutine)从不同的分区中消费者消费数据,但是 ...

  2. Java多线程学习(七)并发编程中一些问题

    本节思维导图: 关注微信公众号:"Java面试通关手册" 回复"Java多线程"获取思维导图源文件和思维导图软件. 多线程就一定好吗?快吗?? 并发编程的目的就 ...

  3. [书籍翻译] 《JavaScript并发编程》第七章 抽取并发逻辑

    本文是我翻译<JavaScript Concurrency>书籍的第七章 抽取并发逻辑,该书主要以Promises.Generator.Web workers等技术来讲解JavaScrip ...

  4. Python并发编程之从生成器使用入门协程(七)

    大家好,并发编程 进入第七篇. 从今天开始,我们将开始进入Python的难点,那就是协程. 为了写明白协程的知识点,我查阅了网上的很多相关资料.发现很难有一个讲得系统,讲得全面的文章,导致我们在学习的 ...

  5. 并发编程(七)——AbstractQueuedSynchronizer 之 CountDownLatch、CyclicBarrier、Semaphore 源码分析

    这篇,我们的关注点是 AQS 最后的部分,共享模式的使用.本文先用 CountDownLatch 将共享模式说清楚,然后顺着把其他 AQS 相关的类 CyclicBarrier.Semaphore 的 ...

  6. java并发编程笔记(七)——线程池

    java并发编程笔记(七)--线程池 new Thread弊端 每次new Thread新建对象,性能差 线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或者OOM 缺 ...

  7. java并发编程工具类JUC第七篇:BlockingDeque双端阻塞队列

    在之前的文章中已经为大家介绍了java并发编程的工具:BlockingQueue接口.ArrayBlockingQueue.DelayQueue.LinkedBlockingQueue.Priorit ...

  8. 1、网络并发编程--简介、软件开发架构、OSI七层协议

    python复习 变量与常量 基本数据类型 内置方法 字符编码.文件操作 函数 函数参数.闭包函数.装饰器 面向对象 封装.继承.多态 """ 什么是对象 数据与功能的结 ...

  9. java并发编程(七)synchronized详解

    Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码.     一.当两个并发线程访问同一个对象object中的这个synchronized( ...

随机推荐

  1. 基于Tensorflow + Opencv 实现CNN自定义图像分类

    摘要:本篇文章主要通过Tensorflow+Opencv实现CNN自定义图像分类案例,它能解决我们现实论文或实践中的图像分类问题,并与机器学习的图像分类算法进行对比实验. 本文分享自华为云社区< ...

  2. spring入门2-aop和集成测试

    1.AOP开发 1.1.简述 作用:面向切面编程:在程序运行期间,在不修改源码的情况下对代码进行增强 优势:减少代码重复,提高开发效率,便于维护 底层:动态代理实现(jdk动态代理,cglib动态代理 ...

  3. Charles的breakpoint功能

    修改请求报文 比如,前端已经控制了输入内容,而我们需要验证接口是否做了校验,这时候怎么测试? 可以通过charles抓包,修改请求报文,修改为在页面上无法输入的内容,发出去然后看后台怎么处理. 修改返 ...

  4. delete,drop,truncate 区别

    今天看到一篇关于delete.drop.truncate区别的文章,认为写得非常好,转过来. 打比方很形象. delete,drop,truncate 都有删除表的作用,区别在于: 1.delete ...

  5. [转载]session多服务器共享的方案梳理

    转载网址: http://www.cnblogs.com/wangtao_20/archive/2013/10/29/3395518.html session的存储了解以前是怎么做的,搞清楚了来龙去脉 ...

  6. P5644-[PKUWC2018]猎人杀【NTT,分治】

    正题 题目链接:https://www.luogu.com.cn/problem/P5644 题目大意 \(n\)个人,每个人被选中的权重是\(a_i\).每次按照权重选择一个没有死掉的人杀死,求第\ ...

  7. GDOI2021划水记

    Day0 上午有意志行,一大早就醒了,然后走了五个小时脚痛.中午洗澡,宿舍轮流看巨人最终话然后聊了一个小时? 下午老师带着我和全爷先开溜,宿舍好像很破旧还还没得充电,领了牌牌和斐爷去吃饭. 然后六点多 ...

  8. mybatis plus 一对多,多表联查的使用小记

    阅读本博文需要有基础的mybatis以及mybatis plus知识,如果没有建议您了解相关的内容 本项目使用的是springboot构建的,数据库字段命名不严谨仅做演示测试使用,本文不做相关源码的解 ...

  9. 改头换面为哪般,最像Android的Windows——Win11升级安装体验

    在过完了十一小长假之后,各位打工人.学僧党可期待的不仅仅是新一轮的工作,Windows11也在10月5日悄悄正式发布,正式版已经面向MSDN订阅用户开放下载. 作为微软金牌合作伙伴,本葡萄已在第一时间 ...

  10. mysql-router-MIC-8.0.26集群部署

    1.具体部署详情请看视频 https://space.bilibili.com/677825194 2.mysql主要配置如下 cat > /etc/my.cnf <<EOF [cl ...