当前位置: 首页 > news >正文

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发现 donetrue 时,并不能确保它能看到 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 源码&#xff0c;并由此引申&#xff0c;简单讨论了单例模式的实现、 atomic 包的作用和 Java volatile 的使用。 sync.Once 使用例子 sync.Once 用于保证一个函数只被调用一次。它可以用于实现单例模式。 有如下类型&#xff1a; type instanc…...

C++面向对象(上)

文章目录前言1.面向过程和面向对象初步认识2.引入类的概念1.概念与用法2.类的访问限定符及封装3.类的作用域和实例化4.类的大小计算5.this指针3.总结前言 本文将对C面向对象进行初步介绍&#xff0c;引入类和对象的概念。围绕类和对象介绍一些基础知识&#xff0c;为以后深入学…...

经常用但是不知道什么是BFC?

BFC学习 block formatting context 块级格式上下文 简单理解&#xff1a; 一个独立容器&#xff0c;内部布局不会受到外面的影响 形成条件&#xff1a; 1.浮动元素&#xff1a;float除none之外的值 2.绝对定位&#xff1a;position:absolute,fixed 3.display:inline-blo…...

GO的临时对象池sync.Pool

GO的临时对象池sync.Pool 文章目录GO的临时对象池sync.Pool一、临时对象池&#xff1a;sync.Pool1.1 临时对象的特点1.2 临时对象池的用途1.3 sync.Pool 的用法二、临时对象池中的值会被及时清理掉2.1 池清理函数2.2 池汇总列表2.3 临时对象池存储值所用的数据结构2.4 临时对象…...

高精度算法一

目录 1. 基础知识 2. 大整数 大整数 3. 大整数 - 大整数 1. 基础知识 利用计算机进行数值计算&#xff0c;有时会遇到这样的问题&#xff1a;有些计算要求精度高&#xff0c;希望计算的数的位数可达几十位甚至几百位&#xff0c;虽然计算机的计算精度也算较高了&#xff0c…...

2023年全国最新食品安全管理员精选真题及答案1

百分百题库提供食品安全管理员考试试题、食品安全员考试预测题、食品安全管理员考试真题、食品安全员证考试题库等&#xff0c;提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 11.预包装食品的标签内容应使用规范的汉字&#xff0c;但可以同时使用&a…...

C++入门:引用

目录 一. 什么是引用 1.1 引用的概念 1.2 引用的定义 二. 引用的性质和用途 2.1 引用的三大主要性质 2.2 引用的主要应用 三. 引用的效率测试 3.1 传值调用和传引用调用的效率对比 3.2 值返回和引用返回的效率对比 四. 常引用 4.1 权限放大和权限缩小问题 4.2 跨…...

SpringSecurity的权限校验详解说明(附完整代码)

说明 SpringSecurity的权限校是基于SpringSecurity的安全认证的详解说明(附完整代码) &#xff08;https://blog.csdn.net/qq_51076413/article/details/129102660&#xff09;的讲解&#xff0c;如果不了解SpringSecurity是怎么认证&#xff0c;请先看下【SpringSecurity的安…...

Java-集合(5)

Map接口 JDK8 Map接口实现子类的特点 Map和Collection是并列关系&#xff0c;Map用于保存具有映射关系的数据&#xff1a;Key-ValueMap中的key和value可以是任何引用类型的数据&#xff0c;会封装到HashMap$Node对象中Map中的key不允许重复&#xff0c;原因和HashSet一样Map…...

研制过程评审活动(四)设计定型阶段

1、设计定型阶段主要任务 设计定型的主要任务是对武器装备性能和使用要求进行全面考核,以确认产品是否达到《研制任务书》和《研制合同》的要求。   设计定型阶段应最终确定《产品规范》、《工艺规范》和《材料规范》的正式版本,并形成正式的全套生产图样、有关技术文件及目…...

【Linux】进程替换

文章目录进程程序替换替换原理替换函数函数返回值函数命名理解在makefile文件中一次生成两个可执行文件总结:程序替换时运行其它语言程序进程程序替换 程序要运行要先加载到内存当中 , 如何做到? 加载器加载进来,然后程序替换 为什么? ->冯诺依曼 因为CPU读取数据的时候只…...

LeetCode171-Excel表列序号(进制转换问题)

LeetCode171-Excel表列序号1、问题描述2、解题思路&#xff1a;进制转换3、代码实现1、问题描述 给你一个字符串columnTitle,表示Excel表格中得列名称。返回该列名称对应得列序号。 例如&#xff1a; A -> 1 B -> 2 C -> 3 ... Z -> 26 AA -> 27 AB -> 28 …...

React SSR

ReactDOMServer 参考链接&#xff1a;https://zh-hans.reactjs.org/docs/react-dom-server.html ReactDOMServer 对象允许你将组件渲染成静态标记。通常&#xff0c;它被使用在 Node 服务端上 // ES modules import * as ReactDOMServer from react-dom/server; // CommonJS v…...

如何系统地优化页面性能

页面优化&#xff0c;其实就是要让页面更快地显示和响应。由于一个页面在它不同的阶段&#xff0c;所侧重的关注点是不一样的&#xff0c;所以如果要讨论页面优化&#xff0c;就要分析一个页面生存周期的不同阶段。 通常一个页面有三个阶段&#xff1a;加载阶段、交互阶段和关…...

Vulnhub 渗透练习(八)—— THE ETHER: EVILSCIENCE

环境搭建 环境下载 靶机和攻击机网络适配都选 NAT 即可。 信息收集 主机扫描 两个端口&#xff0c;22 和 80&#xff0c;且 apache httpd 2.4.0~2.4.29 存在换行解析漏洞。 Apache HTTPD是一款HTTP服务器&#xff0c;它可以通过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 应用层&#xff1a;三套接口select、poll、epoll 3.2 驱动层&#xff1a;实现poll函数 四、信号驱动 4.1 应用层&#xff1a;信号注册fcntl 4.2 驱动层&#xff1a;实现fasync函数 一、…...

看见统计——第三章 概率分布

看见统计——第三章 概率分布 参考 https://github.com/seeingtheory/Seeing-Theory中心极限定理 概率分布描述了随机变量取值的规律。 随机变量Random Variables &#x1f525; 定义&#xff1a;将样本空间中的结果映射到实数的函数 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设计界面运行结果展示...

基于FPGA的PID算法学习———实现PID比例控制算法

基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容&#xff1a;参考网站&#xff1a; PID算法控制 PID即&#xff1a;Proportional&#xff08;比例&#xff09;、Integral&#xff08;积分&…...

8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂

蛋白质结合剂&#xff08;如抗体、抑制肽&#xff09;在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上&#xff0c;高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术&#xff0c;但这类方法普遍面临资源消耗巨大、研发周期冗长…...

如何在看板中体现优先级变化

在看板中有效体现优先级变化的关键措施包括&#xff1a;采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中&#xff0c;设置任务排序规则尤其重要&#xff0c;因为它让看板视觉上直观地体…...

如何在看板中有效管理突发紧急任务

在看板中有效管理突发紧急任务需要&#xff1a;设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP&#xff08;Work-in-Progress&#xff09;弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中&#xff0c;设立专门的紧急任务通道尤为重要&#xff0c;这能…...

【论文笔记】若干矿井粉尘检测算法概述

总的来说&#xff0c;传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度&#xff0c;通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...

【开发技术】.Net使用FFmpeg视频特定帧上绘制内容

目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法&#xff0c;当前调用一个医疗行业的AI识别算法后返回…...

HDFS分布式存储 zookeeper

hadoop介绍 狭义上hadoop是指apache的一款开源软件 用java语言实现开源框架&#xff0c;允许使用简单的变成模型跨计算机对大型集群进行分布式处理&#xff08;1.海量的数据存储 2.海量数据的计算&#xff09;Hadoop核心组件 hdfs&#xff08;分布式文件存储系统&#xff09;&a…...

return this;返回的是谁

一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请&#xff0c;不同级别的经理有不同的审批权限&#xff1a; // 抽象处理者&#xff1a;审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...

面向无人机海岸带生态系统监测的语义分割基准数据集

描述&#xff1a;海岸带生态系统的监测是维护生态平衡和可持续发展的重要任务。语义分割技术在遥感影像中的应用为海岸带生态系统的精准监测提供了有效手段。然而&#xff0c;目前该领域仍面临一个挑战&#xff0c;即缺乏公开的专门面向海岸带生态系统的语义分割基准数据集。受…...

Qemu arm操作系统开发环境

使用qemu虚拟arm硬件比较合适。 步骤如下&#xff1a; 安装qemu apt install qemu-system安装aarch64-none-elf-gcc 需要手动下载&#xff0c;下载地址&#xff1a;https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x…...