一文解析Golangsync.Once用法及原理
本篇文章向大家介绍《一文解析Golangsync.Once用法及原理》,主要包括sync.Once,具有一定的参考价值,需要的朋友可以参考一下。
1. 定位
Once is an object that will perform exactly one action.
sync.Once 是 Go 标准库提供的使函数只执行一次的实现,常应用于单例模式,例如初始化配置、保持数据库连接等。它可以在代码的任意位置初始化和调用,因此可以延迟到使用时再执行,并发场景下是线程安全的。
2. 对外接口
Once 对外仅暴露了唯一的方法 Do(f func()),f 为需要执行的函数。
// Do calls the function f if and only if Do is being called for the
// first time for this instance of Once. In other words, given
// var once Once
// if once.Do(f) is called multiple times, only the first call will invoke f,
// even if f has a different value in each invocation. A new instance of
// Once is required for each function to execute.
//
// Do is intended for initialization that must be run exactly once. Since f
// is niladic, it may be necessary to use a function literal to capture the
// arguments to a function to be invoked by Do:
// config.once.Do(func() { config.init(filename) })
//
// Because no call to Do returns until the one call to f returns, if f causes
// Do to be called, it will deadlock.
//
// If f panics, Do considers it to have returned; future calls of Do return
// without calling f.
//
func (o *Once) Do(f func())
结合注释,我们来看看 Do 方法有哪些需要注意的:
- 只有在当前的 Once 实例第一次调用
Do方法时,才会真正执行f。哪怕在多次调用Do中间f的值有所变化,也只会被实际调用一次; Do针对的是只希望执行一次的初始化操作,由于f是没有参数的,如果需要传参,可以采用包装一层 func 的形式来实现:config.once.Do(func() { config.init(filename) })- 在对
f的调用返回之前,不会返回对Do的调用,所以如果f方法中又调用来Do方法,将会死锁。所以不要做这样的操作:
func main() {
var once sync.Once
once.Do(func() {
once.Do(func() {
fmt.Println("hello kenmawr.")
})
})
}
- 如果
f抛出了 panic,此时Do会认为f已经返回,后续多次调用Do也不会再触发对f的调用。
3. 实战用法
sync.Once 的场景很多,但万变不离其宗的落脚点在于:任何只希望执行一次的操作。
基于此,我们可以发现很多具体的应用场景落地,比如某个资源的清理,全局变量的初始化,单例模式等,它们本质都是一样的。这里简单列几个,大家可以直接参考代码熟悉。
3.1 初始化
很多同学可能会有疑问,我直接在 init() 函数里面做初始化不就可以了吗?效果上是一样的,为什么还要用 sync.Once,这样还需要多声明一个 once 对象。
原因在于:init() 函数是在所在包首次被加载时执行,若未实际使用,既浪费了内存,又延缓了程序启动时间。而 sync.Once 可以在任何位置调用,而且是并发安全的,我们可以在实际依赖某个变量时才去初始化,这样「延迟初始化」从功能上讲并无差异,但可以有效地减少不必要的性能浪费。
我们来看 Golang 官方的 html 库中的一个例子,我们经常使用的转义字符串函数
func UnescapeString(s string) string
在进入函数的时候,首先就会依赖包里内置的 populateMapsOnce 实例(本质是一个 sync.Once) 来执行初始化 entity 的操作。这里的entity是一个包含上千键值对的 map,如果init()时就去初始化,会浪费内存。
var populateMapsOnce sync.Once
var entity map[string]rune
func populateMaps() {
entity = map[string]rune{
"AElig;": '\U000000C6',
"AMP;": '\U00000026',
"Aacute;": '\U000000C1',
"Abreve;": '\U00000102',
"Acirc;": '\U000000C2',
// 省略后续键值对
}
}
func UnescapeString(s string) string {
populateMapsOnce.Do(populateMaps)
i := strings.IndexByte(s, '&')
if i
<h3>3.2 单例模式</h3>
<p>开发中我们经常会实现 <code>Getter</code> 来暴露某个非导出的变量,这个时候就可以把 <code>once.Do</code> 放到 <code>Getter</code> 里面,完成单例的创建。</p>
<pre class="brush:go;">package main
import (
"fmt"
"sync"
)
type Singleton struct{}
var singleton *Singleton
var once sync.Once
func GetSingletonObj() *Singleton {
once.Do(func() {
fmt.Println("Create Obj")
singleton = new(Singleton)
})
return singleton
}
func main() {
var wg sync.WaitGroup
for i := 0; i
<h3>3.3 关闭channel</h3>
<p>一个channel如果已经被关闭,再去关闭的话会 panic,此时就可以应用 sync.Once 来帮忙。</p>
<pre class="brush:go;">type T int
type MyChannel struct {
c chan T
once sync.Once
}
func (m *MyChannel) SafeClose() {
// 保证只关闭一次channel
m.once.Do(func() {
close(m.c)
})
}
4. 原理
在 sync 的源码包中,Once 的定义是一个 struct,所有定义和实现去掉注释后不过 30行,我们直接上源码来分析:
package sync
import (
"sync/atomic"
)
// 一个 Once 实例在使用之后不能被拷贝继续使用
type Once struct {
done uint32 // done 表明了动作是否已经执行
m Mutex
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
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()
}
}
这里有两个非常巧妙的设计值得学习,我们参照注释来看一下:
- 结构体字段顺序对速度的影响 我们来看一下带注释的 Once 结构定义
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
}
sync.Once绝大多数场景都会访问o.done,访问 done 的机器指令是处于hot path上,hot path表示程序非常频繁执行的一系列指令。由于结构体第一个字段的地址和结构体的指针是相同的,如果是第一个字段,直接对结构体的指针解引用即可,如果是其他的字段,除了结构体指针外,还需要计算与第一个值的偏移,所以将done放在第一个字段,则CPU减少了一次偏移量的计算,访问速度更快。
- 为何不使用 CAS 来达到执行一次的效果
其实使用 atomic.CompareAndSwapUint32 是一个非常直观的方案,这样的话 Do 的实现就变成了
func (o *OnceA) Do(f func()) {
if !atomic.CompareAndSwapUint32(&o.done, 0, 1) {
return
}
f()
}
这样的问题在于,一旦出现 CAS 失败的情况,成功协程会继续执行 f,但其他失败协程不会等待 f 执行结束。而Do 的API定位对此有着强要求,当一次 once.Do 返回时,执行的 f 一定是完成的状态。
对此,sync.Once 官方给出的解法是:
Slow path falls back to a mutex, and the atomic.StoreUint32 must be delayed until after f returns.
我们再来结合 doSlow() 看一看这里是怎么解决这个并发问题的:
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
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()
}
}
atomic.LoadUint32用于原子加载地址(也就是 &o.done),返回加载到的值;- o.done 为 0 是代表尚未执行。若同时有两个 goroutine 进来,发现 o.done 为 0(此时
f尚未执行),就会进入o.doSlow(f)的慢路径中(slow path); doSlow使用sync.Mutex来加锁,一个协程进去,其他的被阻塞在锁的地方(注意,此时是阻塞,不是直接返回,这是和 CAS 方案最大的差别);- 经过
o.m.Lock()获取到锁以后,如果此时 o.done 还是 0,意味着依然没有被执行,此时就可以放心的调用f来执行了。否则,说明当前协程在被阻塞的过程中,已经失去了调用f的机会,直接返回。 defer atomic.StoreUint32(&o.done, 1)是这里的精华,必须等到f()返回,在 defer 里才能够去更新 o.done 的值为 1。
5. 避坑
- 不要拷贝一个 sync.Once 使用或作为参数传递,然后去执行
Do,值传递时done会归0,无法起到限制一次的效果。 - 不要在
Do的f中嵌套调用Do。
到这里,我们也就讲完了《一文解析Golangsync.Once用法及原理》的内容了。个人认为,基础知识的学习和巩固,是为了更好的将其运用到项目中,欢迎关注golang学习网公众号,带你了解更多关于golang的知识点!
GoREFLECTLibrary反射类型详解
- 上一篇
- GoREFLECTLibrary反射类型详解
- 下一篇
- Golang 手写一个简单的并发任务 manager
-
- Golang · Go教程 | 6分钟前 |
- Golang中t.Error与t.Fatal区别解析
- 391浏览 收藏
-
- Golang · Go教程 | 20分钟前 |
- Golang构建BFF模式,多端定制后端方案
- 386浏览 收藏
-
- Golang · Go教程 | 35分钟前 |
- Golang实现分布式锁:RedisRedlock算法解析
- 226浏览 收藏
-
- Golang · Go教程 | 49分钟前 |
- Golang函数与方法区别详解
- 291浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- GolangJSON优化:json-iterator替代标准库方法
- 344浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Golangdefer执行时机与使用误区解析
- 348浏览 收藏
-
- Golang · Go教程 | 1小时前 | golang 并发编程 Goroutine channel fan-infan-out
- Golang实现并发模式详解
- 438浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- 使用Gomock模拟返回值,实现精准单元测试
- 129浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- 高级语言转C/C++:内存与运行时问题解析
- 327浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- MongoDB查询为空?BSON配置全解析
- 464浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 3179次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 3390次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 3418次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 4525次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 3798次使用
-
- Go并发编程之sync.Once使用实例详解
- 2022-12-27 484浏览
-
- Go语言并发编程 sync.Once
- 2023-01-27 129浏览
-
- Golang sync.Once实现单例模式的方法详解
- 2023-05-12 319浏览

