当前位置: 首页 > 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设计界面运行结果展示...

网络编程(Modbus进阶)

思维导图 Modbus RTU&#xff08;先学一点理论&#xff09; 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议&#xff0c;由 Modicon 公司&#xff08;现施耐德电气&#xff09;于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...

linux之kylin系统nginx的安装

一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源&#xff08;HTML/CSS/图片等&#xff09;&#xff0c;响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址&#xff0c;提高安全性 3.负载均衡服务器 支持多种策略分发流量…...

springboot 百货中心供应链管理系统小程序

一、前言 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;百货中心供应链管理系统被用户普遍使用&#xff0c;为方…...

ESP32读取DHT11温湿度数据

芯片&#xff1a;ESP32 环境&#xff1a;Arduino 一、安装DHT11传感器库 红框的库&#xff0c;别安装错了 二、代码 注意&#xff0c;DATA口要连接在D15上 #include "DHT.h" // 包含DHT库#define DHTPIN 15 // 定义DHT11数据引脚连接到ESP32的GPIO15 #define D…...

智能在线客服平台:数字化时代企业连接用户的 AI 中枢

随着互联网技术的飞速发展&#xff0c;消费者期望能够随时随地与企业进行交流。在线客服平台作为连接企业与客户的重要桥梁&#xff0c;不仅优化了客户体验&#xff0c;还提升了企业的服务效率和市场竞争力。本文将探讨在线客服平台的重要性、技术进展、实际应用&#xff0c;并…...

React---day11

14.4 react-redux第三方库 提供connect、thunk之类的函数 以获取一个banner数据为例子 store&#xff1a; 我们在使用异步的时候理应是要使用中间件的&#xff0c;但是configureStore 已经自动集成了 redux-thunk&#xff0c;注意action里面要返回函数 import { configureS…...

JVM虚拟机:内存结构、垃圾回收、性能优化

1、JVM虚拟机的简介 Java 虚拟机(Java Virtual Machine 简称:JVM)是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境,实现了 Java 程序的跨平台特性。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的目标代码(字节码),就可以…...

保姆级教程:在无网络无显卡的Windows电脑的vscode本地部署deepseek

文章目录 1 前言2 部署流程2.1 准备工作2.2 Ollama2.2.1 使用有网络的电脑下载Ollama2.2.2 安装Ollama&#xff08;有网络的电脑&#xff09;2.2.3 安装Ollama&#xff08;无网络的电脑&#xff09;2.2.4 安装验证2.2.5 修改大模型安装位置2.2.6 下载Deepseek模型 2.3 将deepse…...

[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】&#xff0c;分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...

深入浅出Diffusion模型:从原理到实践的全方位教程

I. 引言&#xff1a;生成式AI的黎明 – Diffusion模型是什么&#xff1f; 近年来&#xff0c;生成式人工智能&#xff08;Generative AI&#xff09;领域取得了爆炸性的进展&#xff0c;模型能够根据简单的文本提示创作出逼真的图像、连贯的文本&#xff0c;乃至更多令人惊叹的…...