Golang sync.Once 源码浅析
本文分析了Golang sync.Once 源码,并由此引申,简单讨论了单例模式的实现、 atomic 包的作用和 Java volatile 的使用。
sync.Once 使用例子
sync.Once 用于保证一个函数只被调用一次。它可以用于实现单例模式。
有如下类型:
type instance struct {val int
}
假设我们需要单例模式,且需要将 instance 的初始化延迟到第一次访问它的时候,那么可以用 sync.Once:只需将单例的初始化函数传给 Once.Do,便可确保 initSingleton() 恰好执行一次。
var s *instance
var once sync.Oncefunc initSingleton() {s = new(instance)fmt.Println("instance is initializing...")time.Sleep(time.Second)s.val++
}func GetInstance() *instance {once.Do(initSingleton)return s
}
多个 goroutine 并发调用 GetInstance() 仍能保证 initSingleton() 恰好执行一次。
sync.Once 实现原理
sync.Once 内部非常简单,只有一个标识传入的函数是否已经执行的无符号整型,以及一个互斥锁。
type Once struct {done uint32m Mutex
}
由上述使用例子,多个 goroutine 调用 Do 仍能保证传入的函数恰好被执行一次。 Do 首先检查其 done 成员是否为零,若为零,说明初始化还未完成,这时加锁,重新检查 done 的值确保还未初始化,并调用初始化函数 f()。调用返回后,将 done 修改为1,指示已经初始化。
func (o *Once) Do(f func()) {if atomic.LoadUint32(&o.done) == 0 {// Outlined slow-path to allow inlining of the fast-path.o.doSlow(f)}
}func (o *Once) doSlow(f func()) {o.m.Lock()defer o.m.Unlock()if o.done == 0 {defer atomic.StoreUint32(&o.done, 1)f()}
}
多个 goroutine 同时调用 Once.Do 会发生什么?
假设多个 goroutine 发现 done 的值为零,同时进入了 doSlow 方法,因为 doSlow 方法需要加锁,只有一个 goroutine 能够执行 f(),其余 goroutine 将阻塞。当执行 f() 的 goroutine 返回前更新 done 值后解锁,其余 goroutine 能够继续执行 doSlow,再次检查 done,发现已经不为零,说明在等待锁的间隙已经有其它 goroutine 调用 f() 完成了初始化,当前 goroutine 解锁并返回。
为什么加了锁之后不需要用原子读取函数 atomic.LoadUint32?
这是因为互斥锁 m 保护了 done 字段不会被并发修改、读取。可以安全地读取 done。不同的是,doSlow 之前对 done 的读取必须是原子读取,否则这里将存在一个 data race。
为什么加锁后仍要用 atomic.StoreUint32,而不是直接赋值 done = 1?
因为 done 不是 volatile 的,直接赋值无法保证可见性。也不能确保 done = 1 不被重排序到 f() 之前。关于 atomic load/store,参考如下:
What is the point of sync/atomic.(Load|Store)Int32 ?
However, the atomic load and store provide another property. If one processor executes “a = 1; b = 1” (let’s say that a and b are always 0 before) and another processor executes “if b { c = a }” then if the “b = 1” uses a non-atomic store, or the “if b” uses a non-atomic load, then it is entirely possible that the second processor will read the old value of a and set c to 0. That is, using a non-atomic load or store does not provide any ordering guarantees with regard to other memory that may have been set by the other processor.
You almost never care about only atomicity. There is also ordering (as Ian described) and visibility (loads/stores must be visible to other goroutines in a finite amount of time, this is not true for non-atomic loads/store). And there are also data races, which render behavior of your program undefined. All the same applies to C/C++ as well.
Why supporting atomic.Load and atomic.Store in Go?
Because of ordering guarantees, and memory operation visibility. For instance:
y:=0
x:=0
x=1
y=1
In the above program, another goroutine can see (0,0), (0,1), (1,0), or (1,1) for x and y. This is because of compiler reordering the code, compiler optimization,s or because of memory operation reordering at the hardware level. However:
y:=0
x:=0
x:=1
atomic.StoreInt64(&y,1)
If another goroutine sees atomic.LoadInt64(&y)==1, then the goroutine is guaranteed to see x=1.
为什么不能 atomic.CompareAndSwapUint32(&o.done, 0, 1) 判断为 true 后直接调用 f() 初始化?
如下所示:
func (o *Once) Do(f func()) {if atomic.CompareAndSwapUint32(&o.done, 0, 1) {f()}
}
多个 goroutine 进入 Do 时,能够保证 f() 只被调用一次,但是不能保证 goroutine 返回时初始化已经完成。但是这种方法可以用于 Once 的异步实现。即一个 goroutine 发现该实例还未初始化完成,立刻返回并继续做其他事情。
单例的错误实现
sync.Once 利用 atomic 包实现了「只调用一次」的语义。可以只用一个互斥锁,先判断是否初始化,如果还没初始化,加锁,再判断是否已经初始化,才进行初始化。如下 GetInstanceV2() 所示。
package singletonimport ("sync"
)type instance struct {val int
}var s *instance
var once sync.Once
var mu sync.Mutexfunc initSingleton() {s = new(instance)fmt.Println("instance is initializing...")time.Sleep(time.Second)s.val++
}func GetInstance() *instance {once.Do(initSingleton)return s
}func GetInstanceV2() *instance {// 先不加锁判断if s == nil {// 未初始化,加锁mu.Lock()defer mu.Unlock()// 加锁后重新判断if s == nil {// 进行初始化initSingleton()}}return s
}
事实上,在 GetInstanceV2 中第一次读取 s 没有加锁,又因为 s 不是 volatile 类型的(Go 也没有 volatile),当能够看到 s != nil 时,也不能保证 s 已经初始化完成,所以 GetInstanceV2 实现是有问题的。如果用 Java 实现,可以将 s 声明为 volatile,那么某线程初始化给 s 赋值后,其它线程能立刻看到 s != null。
为了验证上述例子存在并发问题,编写测试用例如下:
func TestGetInstanceV2(t *testing.T) {var wg sync.WaitGroupfor i := 0; i < 100; i++ {wg.Add(1)go func() {GetInstanceV2()wg.Done()}()}wg.Wait()assert.True(t, s.val == 1)
}
上述测试用例创建了 100 个 goroutine 同时调用 GetInstanceV2。
测试如下:
go test -v -race -run TestGetInstanceV2=== RUN TestGetInstanceV2
==================
WARNING: DATA RACE
Read at 0x0000014380a8 by goroutine 9:...
Previous write at 0x0000014380a8 by goroutine 8:...
Goroutine 9 (running) created at:...
Goroutine 8 (finished) created at:...
==================testing.go:1312: race detected during execution of test
--- FAIL: TestGetInstanceV2 (0.01s)
=== CONT testing.go:1312: race detected during execution of test
FAIL
exit status 1
上述报错说明了问题的存在。
Java 单例模式实现
附上 Java 的单例模式,实例必须声明为 volatile:
public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }
}
类似错误情形
情形一
在 The Official Golang Blog 中描述了类似的情形:
Double-checked locking is an attempt to avoid the overhead of synchronization. For example, the twoprint program might be incorrectly written as:
var a string
var done boolfunc setup() {// 先赋值,后设置 donea = "hello, world"done = true
}func doprint() {if !done {once.Do(setup)}print(a)
}func twoprint() {go doprint()go doprint()
}
but there is no guarantee that, in doprint, observing the write to done implies observing the write to a. This version can (incorrectly) print an empty string instead of “hello, world”.
意思是说,doprint发现 done 为 true 时,并不能确保它能看到 a 的值已经初始化。没有同步保证 a 先初始化,再设置 done。
情形二
Another incorrect idiom is busy waiting for a value, as in:
var a string
var done boolfunc setup() {a = "hello, world"done = true
}func main() {go setup()for !done {}print(a)
}
As before, there is no guarantee that, in main, observing the write to done implies observing the write to a, so this program could print an empty string too. Worse, there is no guarantee that the write to done will ever be observed by main, since there are no synchronization events between the two threads. The loop in main is not guaranteed to finish.
这是上一个例子的 busy waiting 变种,同样不能保证 a 先初始化再设置 done。
情形三
There are subtler variants on this theme, such as this program.
type T struct {msg string
}var g *Tfunc setup() {t := new(T)t.msg = "hello, world" // 1g = t // 2
}func main() {go setup()for g == nil {}print(g.msg)
}
Even if main observes g != nil and exits its loop, there is no guarantee that it will observe the initialized value for g.msg.
上述错误更为隐晦,即使 main 发现 g 已经不为 nil 了,也无法保证 g.msg 已经设置,也就是说,不能确保代码中 语句1 和 语句2 的先后顺序。
相关文章:
Golang sync.Once 源码浅析
本文分析了Golang sync.Once 源码,并由此引申,简单讨论了单例模式的实现、 atomic 包的作用和 Java volatile 的使用。 sync.Once 使用例子 sync.Once 用于保证一个函数只被调用一次。它可以用于实现单例模式。 有如下类型: type instanc…...
C++面向对象(上)
文章目录前言1.面向过程和面向对象初步认识2.引入类的概念1.概念与用法2.类的访问限定符及封装3.类的作用域和实例化4.类的大小计算5.this指针3.总结前言 本文将对C面向对象进行初步介绍,引入类和对象的概念。围绕类和对象介绍一些基础知识,为以后深入学…...
经常用但是不知道什么是BFC?
BFC学习 block formatting context 块级格式上下文 简单理解: 一个独立容器,内部布局不会受到外面的影响 形成条件: 1.浮动元素:float除none之外的值 2.绝对定位:position:absolute,fixed 3.display:inline-blo…...
GO的临时对象池sync.Pool
GO的临时对象池sync.Pool 文章目录GO的临时对象池sync.Pool一、临时对象池:sync.Pool1.1 临时对象的特点1.2 临时对象池的用途1.3 sync.Pool 的用法二、临时对象池中的值会被及时清理掉2.1 池清理函数2.2 池汇总列表2.3 临时对象池存储值所用的数据结构2.4 临时对象…...
高精度算法一
目录 1. 基础知识 2. 大整数 大整数 3. 大整数 - 大整数 1. 基础知识 利用计算机进行数值计算,有时会遇到这样的问题:有些计算要求精度高,希望计算的数的位数可达几十位甚至几百位,虽然计算机的计算精度也算较高了,…...
2023年全国最新食品安全管理员精选真题及答案1
百分百题库提供食品安全管理员考试试题、食品安全员考试预测题、食品安全管理员考试真题、食品安全员证考试题库等,提供在线做题刷题,在线模拟考试,助你考试轻松过关。 11.预包装食品的标签内容应使用规范的汉字,但可以同时使用&a…...
C++入门:引用
目录 一. 什么是引用 1.1 引用的概念 1.2 引用的定义 二. 引用的性质和用途 2.1 引用的三大主要性质 2.2 引用的主要应用 三. 引用的效率测试 3.1 传值调用和传引用调用的效率对比 3.2 值返回和引用返回的效率对比 四. 常引用 4.1 权限放大和权限缩小问题 4.2 跨…...
SpringSecurity的权限校验详解说明(附完整代码)
说明 SpringSecurity的权限校是基于SpringSecurity的安全认证的详解说明(附完整代码) (https://blog.csdn.net/qq_51076413/article/details/129102660)的讲解,如果不了解SpringSecurity是怎么认证,请先看下【SpringSecurity的安…...
Java-集合(5)
Map接口 JDK8 Map接口实现子类的特点 Map和Collection是并列关系,Map用于保存具有映射关系的数据:Key-ValueMap中的key和value可以是任何引用类型的数据,会封装到HashMap$Node对象中Map中的key不允许重复,原因和HashSet一样Map…...
研制过程评审活动(四)设计定型阶段
1、设计定型阶段主要任务 设计定型的主要任务是对武器装备性能和使用要求进行全面考核,以确认产品是否达到《研制任务书》和《研制合同》的要求。 设计定型阶段应最终确定《产品规范》、《工艺规范》和《材料规范》的正式版本,并形成正式的全套生产图样、有关技术文件及目…...
【Linux】进程替换
文章目录进程程序替换替换原理替换函数函数返回值函数命名理解在makefile文件中一次生成两个可执行文件总结:程序替换时运行其它语言程序进程程序替换 程序要运行要先加载到内存当中 , 如何做到? 加载器加载进来,然后程序替换 为什么? ->冯诺依曼 因为CPU读取数据的时候只…...
LeetCode171-Excel表列序号(进制转换问题)
LeetCode171-Excel表列序号1、问题描述2、解题思路:进制转换3、代码实现1、问题描述 给你一个字符串columnTitle,表示Excel表格中得列名称。返回该列名称对应得列序号。 例如: A -> 1 B -> 2 C -> 3 ... Z -> 26 AA -> 27 AB -> 28 …...
React SSR
ReactDOMServer 参考链接:https://zh-hans.reactjs.org/docs/react-dom-server.html ReactDOMServer 对象允许你将组件渲染成静态标记。通常,它被使用在 Node 服务端上 // ES modules import * as ReactDOMServer from react-dom/server; // CommonJS v…...
如何系统地优化页面性能
页面优化,其实就是要让页面更快地显示和响应。由于一个页面在它不同的阶段,所侧重的关注点是不一样的,所以如果要讨论页面优化,就要分析一个页面生存周期的不同阶段。 通常一个页面有三个阶段:加载阶段、交互阶段和关…...
Vulnhub 渗透练习(八)—— THE ETHER: EVILSCIENCE
环境搭建 环境下载 靶机和攻击机网络适配都选 NAT 即可。 信息收集 主机扫描 两个端口,22 和 80,且 apache httpd 2.4.0~2.4.29 存在换行解析漏洞。 Apache HTTPD是一款HTTP服务器,它可以通过mod_php来运行PHP网页。其2.4.0~2.4.29版本中…...
华为OD机试题 - 水仙花数 2(JavaScript)| 代码+思路+重要知识点
最近更新的博客 华为OD机试题 - 字符串加密(JavaScript) 华为OD机试题 - 字母消消乐(JavaScript) 华为OD机试题 - 字母计数(JavaScript) 华为OD机试题 - 整数分解(JavaScript) 华为OD机试题 - 单词反转(JavaScript) 使用说明 参加华为od机试,一定要注意不要完全背…...
字符设备驱动基础(二)
目录 一、五种IO模型------读写外设数据的方式 二、阻塞与非阻塞 三、多路复用 3.1 应用层:三套接口select、poll、epoll 3.2 驱动层:实现poll函数 四、信号驱动 4.1 应用层:信号注册fcntl 4.2 驱动层:实现fasync函数 一、…...
看见统计——第三章 概率分布
看见统计——第三章 概率分布 参考 https://github.com/seeingtheory/Seeing-Theory中心极限定理 概率分布描述了随机变量取值的规律。 随机变量Random Variables 🔥 定义:将样本空间中的结果映射到实数的函数 XXX 称为随机变量(random variable)&a…...
【基于众包标注的语文教材句子难易度评估研究 论文精读】
基于众包标注的语文教材句子难易度评估研究 论文精读信息摘 要0 引言1 相关研究2 众包标注方法3 语料库构建3.1 数据收集3.1 基于五点量表的专家标注3.3 基于成对比较的众包标注4 特征及模型4.1 特征抽取4.2 模型与实验设计4.2.1 任务一:单句绝对难度评估4.2.2 任务二:句对相对…...
实例五:MATLAB APP design-APP登录界面的设计
一、APP 界面设计展示 注:在账号和密码提示框输入相应的账号和密码后,点击登录按钮,即可跳转到程序中设计的工作界面。 二、APP设计界面运行结果展示...
家政维修平台实战20:权限设计
目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系,主要是分成几个表,用户表我们是记录用户的基础信息,包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题,不同的角色…...
微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据
微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据 Power Query 具有大量专门帮助您清理和准备数据以供分析的功能。 您将了解如何简化复杂模型、更改数据类型、重命名对象和透视数据。 您还将了解如何分析列,以便知晓哪些列包含有价值的数据,…...
管理学院权限管理系统开发总结
文章目录 🎓 管理学院权限管理系统开发总结 - 现代化Web应用实践之路📝 项目概述🏗️ 技术架构设计后端技术栈前端技术栈 💡 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 🗄️ 数据库设…...
日常一水C
多态 言简意赅:就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过,当子类和父类的函数名相同时,会隐藏父类的同名函数转而调用子类的同名函数,如果要调用父类的同名函数,那么就需要对父类进行引用&#…...
提升移动端网页调试效率:WebDebugX 与常见工具组合实践
在日常移动端开发中,网页调试始终是一个高频但又极具挑战的环节。尤其在面对 iOS 与 Android 的混合技术栈、各种设备差异化行为时,开发者迫切需要一套高效、可靠且跨平台的调试方案。过去,我们或多或少使用过 Chrome DevTools、Remote Debug…...
02.运算符
目录 什么是运算符 算术运算符 1.基本四则运算符 2.增量运算符 3.自增/自减运算符 关系运算符 逻辑运算符 &&:逻辑与 ||:逻辑或 !:逻辑非 短路求值 位运算符 按位与&: 按位或 | 按位取反~ …...
Qt的学习(一)
1.什么是Qt Qt特指用来进行桌面应用开发(电脑上写的程序)涉及到的一套技术Qt无法开发网页前端,也不能开发移动应用。 客户端开发的重要任务:编写和用户交互的界面。一般来说和用户交互的界面,有两种典型风格&…...
ArcGIS Pro+ArcGIS给你的地图加上北回归线!
今天来看ArcGIS Pro和ArcGIS中如何给制作的中国地图或者其他大范围地图加上北回归线。 我们将在ArcGIS Pro和ArcGIS中一同介绍。 1 ArcGIS Pro中设置北回归线 1、在ArcGIS Pro中初步设置好经纬格网等,设置经线、纬线都以10间隔显示。 2、需要插入背会归线…...
echarts使用graphic强行给图增加一个边框(边框根据自己的图形大小设置)- 适用于无法使用dom的样式
pdf-lib https://blog.csdn.net/Shi_haoliu/article/details/148157624?spm1001.2014.3001.5501 为了完成在pdf中导出echarts图,如果边框加在dom上面,pdf-lib导出svg的时候并不会导出边框,所以只能在echarts图上面加边框 grid的边框是在图里…...
Element-Plus:popconfirm与tooltip一起使用不生效?
你们好,我是金金金。 场景 我正在使用Element-plus组件库当中的el-popconfirm和el-tooltip,产品要求是两个需要结合一起使用,也就是鼠标悬浮上去有提示文字,并且点击之后需要出现气泡确认框 代码 <el-popconfirm title"是…...
