前言

Go 版本 1.16
如果本文对你有帮助,给个赞吧;
喜欢本文就收藏一下吧;
有问题欢迎评论留言,基本都会回。

1. sync.Once 简介

sync.Once 是 Go 语言实现的一种对象,用来保证某种行为只会被执行一次。
它只提供一个 API:

func (o *Once) Do(f func())

无论调用多少次 Do,都只有第一次调用生效。

2. sync.Once 源码解析

// Once is an object that will perform exactly one action.
//
// A Once must not be copied after first use.
type Once struct {
// done indicates whether the action has been performed.
// It is first in the struct because it is used in the hot path.
// The hot path is inlined at every call site.
// Placing done first allows more compact instructions on some architectures (amd64/386),
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}

一些重点:

  1. done 字段用来判断某行为 action 是否已进行,因为它 hot path 中被使用,放在结构体的第一个字段能够减少机器指令。
  2. Once 用过了就不要再复制产生副本。

2.1 为什么 done 作为第一个字段

hot path:程序频繁执行的一些指令。
在源码中 done 字段频繁被访问(后面源码分析会讲到),所以它处在 hot path 上。
那为什么作为第一个字段就能减少 CPU 指令、提高性能呢?
因为结构体第一个字段的地址和结构体的地址是一样的,要访问第一个字段直接对结构体指针进行解引用即可,而访问后面的字段就要计算偏移量(前面字段所占字节空间 + 是否进行了内存对齐),就会增加 CPU 指令。

2.2 Do 方法的实现细节

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

源码注释中给出了一种 Do 的错误实现方式:使用 CAS 操作判断 f 是否已经执行,如果没有则执行,否则不执行。
咋看起来没什么问题,源码给出解释:Do 应该保证当自己返回时,f 已经执行完毕。当同时调用两次 Do 时,竞争成功者将原子地把 done 从 0 改为 1,失败者再进行 CAS 操作时发现不满足条件将直接返回,没有等成功者将 f 执行完。
这也就是为什么源码实现要用到互斥锁 mutex 以及为什么 atomic.StoreUint32 操作要等 f 返回后再执行(见下文 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()
}
}

使用 defer 可以保证 f 先执行完,在 doSlow 返回时才执行 atomic.StoreUint32(&o.done, 1),当然 o.m.Unlock() 也是在 doSlow 返回时执行。
注:defer 的执行顺序是后进先出,也就是最后 defer 的函数,在返回时最先被执行。

看完源码,上来就先原子加载 done,上锁后还访问一次 done,因此说 done 在 hot path 上(填坑)。

思考:为什么 atomic.StoreUint32(&o.done, 1) 要用 defer 关键字,而不是直接写在 f() 后面呢?
因为 Once 本身的语义就是对外保证你传进来 f 执行过一次,若 f 在执行过程中 panic 了,会导致 Do 也直接退出,但是退出前会把所有的 defer 都执行完,保证了 f 执行过一次。若放在 f() 后面,当 f 发生 panic 之后,done 就不能置为 1。

2.3 其他重要细节

  1. 问:对于源码中举例的错误实现方式,并发环境下,Do 可能被多次调用,竞争失败者并没有等待成功者的 f 执行完就返回了,那么源码是怎么保证的呢?
    答:源码通过互斥锁保证的,竞争失败者由于没有获得互斥锁会阻塞在 o.m.Lock() 不会立即返回,只有当成功者执行完 f 并释放锁之后,失败者们才能依次获得锁,但由于此时 done 已经被成功者改为 1,失败者们就都不会执行 f 了。

  2. sync.Once 是线程安全的,互斥锁保证只有一个线程能够修改 done 的值。

  3. once.Do(f func()) 方法不能嵌套,若 f 在执行过程中也会调用 once.Do,会导致死锁。原因很简单,f 要获得锁才能执行,而外层的 Do 已经获得并等 f 执行完才能释放锁(我在等你,你在等我,谁也不肯先放手= =)。

3. sync.Once 的应用场景

  1. 配合单例模式,让实例只初始化一次。
var instance int    // 模拟单例模式的实例
var once sync.Once func getInstance() int {
once.Do(func () {
instance = 2
})
return instance
}

Do 只执行一次的特性实现来单例模式的懒汉式加载。

  1. 初始化项目的配置,与 init 函数类似。因为 init 函数只会在 package 被加载时执行一次,若迟迟未被加载,则会浪费内存,所以可以使用 Once 初始化配置。

sync.Once 使用及解析的更多相关文章

  1. go sync.map源码解析

    go中的map是并发不安全的,同时多个协程读取不会出现问题,但是多个协程 同时读写就会出现 fatal error:concurrent map read and map write的错误.通用的解决 ...

  2. 深入解析ReentrantReadWriteLock

    前言: 在Java的锁中很多锁都是同一时刻只允许一个线程访问,今天就来看看一个特殊的锁——读写锁.它的特殊之处就在于同一时刻可以运行多个读线程访问或者有一个写线程在访问.能够大大的提高并发性和吞吐量 ...

  3. 深入浅出ReentrantLock源码解析

    ReentrantLock不但是可重入锁,而且还是公平或非公平锁,在工作中会经常使用到,将自己对这两种锁的理解记录下来,希望对大家有帮助. 前提条件 在理解ReentrantLock时需要具备一些基本 ...

  4. 深入浅出Semaphore源码解析

    Semaphore通过permits的值来限制线程访问临界资源的总数,属于有限制次数的共享锁,不支持重入. 前提条件 在理解Semaphore时需要具备一些基本的知识: 理解AQS的实现原理 之前有写 ...

  5. 【嵌入式开发】嵌入式 开发环境 (远程登录 | 文件共享 | NFS TFTP 服务器 | 串口连接 | Win8.1 + RedHat Enterprise 6.3 + Vmware11)

    作者 : 万境绝尘 博客地址 : http://blog.csdn.net/shulianghan/article/details/42254237 一. 相关工具下载 嵌入式开发工具包 : -- 下 ...

  6. Java并发(十五):并发工具类——信号量Semaphore

    先做总结: 1.Semaphore是什么? Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源. 把它比作是控制流量的红绿灯,比如XX马路要 ...

  7. Ceph 之RGW Pub-Sub Module

    Overview Pub-Sub module 顾名思义是一个发布订阅相关的模块.Pub-Sub module 为对象存储的变更事件提供一种发布-订阅机制.而发布-订阅架构本身应用非常广泛,如公有云G ...

  8. Hbase性能调优(二)

    一.HBase关键参数配置指导 如果同时存在读和写的操作,这两种操作的性能会相互影响.如果写入导致的flush和Compaction操作频繁发生,会占用大量的磁盘IO操作,从而影响读取的性能.如果写入 ...

  9. 十一. Go并发编程--singleflight

    一.前言 1.1 为什么需要Singleflight? 很多程序员可能还是第一次听说,本人第一次听说这个的时候以为翻译过来就是程序设计中被称为的是 "单例模式". google之后 ...

  10. Vue中v-model解析、sync修饰符解析

    上善若水,水善利萬物而不爭.——<道德經> 简介 在平时开发是经常用到一些父子组件通信,经常用到props.vuex等等,这里面记录另外的三种方式v-model.sync是怎么使用,再说是 ...

随机推荐

  1. day09-Tomcat01

    Tomcat01 1.WEB开发介绍 WEB,在英文中WEB表示网/网络资源,它用于表示WEB服务器(主机)供浏览器访问的资源 WEB服务器(主机)上供外界访问的Web资源为: 静态web资源(如ht ...

  2. Modbus协议及python库实现

    基础知识 硬件层协议:解决0和1的可靠传输,常有RS232.RS485.CAN.IIC.SPI - 软件层协议:解决传输目的,常有Modbus.TCP/IP.CANopen - 协议优点: Modbu ...

  3. 真正“搞”懂HTTP协议03之时间穿梭

    上一篇我们简单的介绍了一下DoD模型和OSI模型,还着重的讲解了TCP的三次握手和四次挥手,让我们在空间层面,稍稍宏观的了解了HTTP所依赖的底层模型,那么这一篇,我们来追溯一下HTTP的历史,看一看 ...

  4. IIS 配置集中式证书模块实现网站自动绑定证书文件

    在 Windows 环境下如果采用 IIS 作为 网站服务器时,常规的网站绑定 HTTPS 需要一个一个站点手动选择对应的证书绑定,而且证书过期之后更换证书时也是需要一个个重新绑定操作,无法便捷的做到 ...

  5. JUnit 5 单元测试教程

    点赞再看,动力无限. 微信搜「程序猿阿朗 」. 本文 Github.com/niumoo/JavaNotes 和 未读代码博客 已经收录,有很多知识点和系列文章. 在软件开发过程中,我们通常都需要测试 ...

  6. Codeforces Round #834 (Div. 3) A-G

    比赛链接 A 题目 知识点:模拟. 确定开头字母,然后循环比较即可. 时间复杂度 \(O(n)\) 空间复杂度 \(O(n)\) 题解 #include <bits/stdc++.h> # ...

  7. c++题目:吃西瓜

    吃西瓜 [问题描述] 老胡买了是长方体形的西瓜来犒劳大家.... 这块西瓜长m厘米,宽n厘米,高h厘米.他发现如果把这块西瓜平均地分成m*n*h块1立方厘米的小正方体,那么每一小块都会有一个营养值(可 ...

  8. MyEclipse反编译插件安装于使用

    在MyEclipse开发中,使用反编译插件可以对jar包的源码进行随机的查看,节约了使用jd-gui查看时间. 百度云分享地址:链接:https://pan.baidu.com/s/1efNR6A 密 ...

  9. day10 Test

    public class Test{ public static void main(String[] args){ fun1(); } /**1. * 有2个数组,第一个数组内容为:[黑龙江省,浙江 ...

  10. 关于Qt的QPixmap中不支持jpg文件格式的问题

    问题 Qt部分版本存在不支持jpg,JPEG等图像格式的问题 qDebug()<<QImageWriter::supportedImageFormats(); 这行代码可以查看所支持的图像 ...