Golang并发利器sync.Once的用法详解
作者:陈明勇 发布时间:2024-04-25 15:12:06
简介
在某些场景下,我们需要初始化一些资源,例如单例对象、配置等。实现资源的初始化有多种方法,如定义 package
级别的变量、在 init
函数中进行初始化,或者在 main
函数中进行初始化。这三种方式都能确保并发安全,并在程序启动时完成资源的初始化。
然而,有时我们希望采用延迟初始化的方式,在我们真正需要资源的时候才进行初始化,这种需要确保并发安全,在这种情况下,Go
语言中的 sync.Once
提供一个优雅且并发安全的解决方案,本文将对其进行介绍。
sync.Once 基本概念
什么是 sync.Once
sync.Once
是 Go
语言中的一种同步原语,用于确保某个操作或函数在并发环境下只被执行一次。它只有一个导出的方法,即 Do
,该方法接收一个函数参数。在 Do
方法被调用后,该函数将被执行,而且只会执行一次,即使在多个协程同时调用的情况下也是如此。
sync.Once 的应用场景
sync.Once 主要用于以下场景:
单例模式:确保全局只有一个实例对象,避免重复创建资源。
延迟初始化:在程序运行过程中需要用到某个资源时,通过
sync.Once
动态地初始化该资源。只执行一次的操作:例如只需要执行一次的配置加载、数据清理等操作。
sync.Once 应用实例
单例模式
在单例模式中,我们需要确保一个结构体只被初始化一次。使用 sync.Once
可以轻松实现这一目标。
package main
import (
"fmt"
"sync"
)
type Singleton struct{}
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s := GetInstance()
fmt.Printf("Singleton instance address: %p\n", s)
}()
}
wg.Wait()
}
上述代码中,GetInstance
函数通过 once.Do()
确保 instance
只会被初始化一次。在并发环境下,多个协程同时调用 GetInstance
时,只有一个协程会执行 instance = &Singleton{}
,所有协程得到的实例 s
都是同一个。
延迟初始化
有时候希望在需要时才初始化某些资源。使用 sync.Once
可以实现这一目标。
package main
import (
"fmt"
"sync"
)
type Config struct {
config map[string]string
}
var (
config *Config
once sync.Once
)
func GetConfig() *Config {
once.Do(func() {
fmt.Println("init config...")
config = &Config{
config: map[string]string{
"c1": "v1",
"c2": "v2",
},
}
})
return config
}
func main() {
// 第一次需要获取配置信息,初始化 config
cfg := GetConfig()
fmt.Println("c1: ", cfg.config["c1"])
// 第二次需要,此时 config 已经被初始化过,无需再次初始化
cfg2 := GetConfig()
fmt.Println("c2: ", cfg2.config["c2"])
}
在这个示例中,定义了一个 Config
结构体,它包含一些设置信息。使用 sync.Once
来实现 GetConfig
函数,该函数在第一次调用时初始化 Config
。这样,我们可以在真正需要时才初始化 Config
,从而避免不必要的开销。
sync.Once 实现原理
type Once struct {
// 表示是否执行了操作
done uint32
// 互斥锁,确保多个协程访问时,只能一个协程执行操作
m Mutex
}
func (o *Once) Do(f func()) {
// 判断 done 的值,如果是 0,说明 f 还没有被执行过
if atomic.LoadUint32(&o.done) == 0 {
// 构建慢路径(slow-path),以允许对 Do 方法的快路径(fast-path)进行内联
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
// 加锁
o.m.Lock()
defer o.m.Unlock()
// 双重检查,避免 f 已被执行过
if o.done == 0 {
// 修改 done 的值
defer atomic.StoreUint32(&o.done, 1)
// 执行函数
f()
}
}
sync.Once
结构体包含两个字段:done
和 mu
。done
是一个 uint32
类型的变量,用于表示操作是否已经执行过;m
是一个互斥锁,用于确保在多个协程访问时,只有一个协程能执行操作。
sync.Once
结构体包含两个方法:Do
和 doSlow
。Do
方法是其核心方法,它接收一个函数参数 f
。首先它会通过原子操作atomic.LoadUint32
(保证并发安全) 检查 done
的值,如果为 0,表示 f
函数没有被执行过,然后执行 doSlow
方法。
在 doSlow
方法里,首先对互斥锁 m
进行加锁,确保在多个协程访问时,只有一个协程能执行 f
函数。接着再次检查 done
变量的值,如果 done
的值仍为 0,说明 f
函数没有被执行过,此时执行 f
函数,最后通过原子操作 atomic.StoreUint32
将 done
变量的值设置为 1。
为什么会封装一个 doSlow 方法
doSlow
方法的存在主要是为了性能优化。将慢路径(slow-path
)代码从 Do
方法中分离出来,使得 Do
方法的快路径(fast-path
)能够被内联(inlined
),从而提高性能。
为什么会有双重检查(double check)的写法
从源码可知,存在两次对 done
的值的判断。
第一次检查:在获取锁之前,先使用原子加载操作
atomic.LoadUint32
检查done
变量的值,如果done
的值为 1,表示操作已执行,此时直接返回,不再执行doSlow
方法。这一检查可以避免不必要的锁竞争。第二次检查:获取锁之后,再次检查
done
变量的值,这一检查是为了确保在当前协程获取锁期间,其他协程没有执行过f
函数。如果done
的值仍为 0,表示f
函数没有被执行过。
通过双重检查,可以在大多数情况下避免锁竞争,提高性能。
加强的 sync.Once
sync.Once
提供的 Do
方法并没有返回值,意味着如果我们传入的函数如果发生 error
导致初始化失败,后续调用 Do
方法也不会再初始化。为了避免这个问题,我们可以实现一个 类似 sync.Once
的并发原语。
package main
import (
"sync"
"sync/atomic"
)
type Once struct {
done uint32
m sync.Mutex
}
func (o *Once) Do(f func() error) error {
if atomic.LoadUint32(&o.done) == 0 {
return o.doSlow(f)
}
return nil
}
func (o *Once) doSlow(f func() error) error {
o.m.Lock()
defer o.m.Unlock()
var err error
if o.done == 0 {
err = f()
// 只有没有 error 的时候,才修改 done 的值
if err == nil {
atomic.StoreUint32(&o.done, 1)
}
}
return err
}
上述代码实现了一个加强的 Once
结构体。与标准的 sync.Once
不同,这个实现允许 Do
方法的函数参数返回一个 error
。如果执行函数没有返回 error
,则修改 done
的值以表示函数已执行。这样,在后续的调用中,只有在没有发生 error
的情况下,才会跳过函数执行,避免初始化失败。
sync.Once 的注意事项
死锁
通过分析 sync.Once
的源码,可以看到它包含一个名为 m
的互斥锁字段。当我们在 Do
方法内部重复调用 Do
方法时,将会多次尝试获取相同的锁。但是 mutex
互斥锁并不支持可重入操作,因此这将导致死锁现象。
func main() {
once := sync.Once{}
once.Do(func() {
once.Do(func() {
fmt.Println("init...")
})
})
}
初始化失败
这里的初始化失败指的是在调用 Do
方法之后,执行 f
函数的过程中发生 error
,导致执行失败,现有的 sync.Once
设计我们是无法感知到初始化的失败的,为了解决这个问题,我们可以实现一个类似 sync.Once
的加强 once
,前面的内容已经提供了具体实现。
小结
本文详细介绍了 Go
语言中的 sync.Once
,包括它的基本定义、使用场景和应用实例以及源码分析等。在实际开发中,sync.Once
经常被用于实现单例模式和延迟初始化操作。
虽然 sync.Once
简单而又高效,但是错误的使用可能会造成一些意外情况,需要格外小心。
总之,sync.Once
是 Go
中非常实用的一个并发原语,可以帮助开发者实现各种并发场景下的安全操作。如果遇到只需要初始化一次的场景,sync.Once
是一个非常好的选择。
来源:https://juejin.cn/post/7220797267716358199
猜你喜欢
- 因笔者个人需要需要在本机安装Mysql,先将安装过程记录如下,希望对他人有所参考。 一、下载软件1. 进入mysql官网,登陆自己
- 最近,由于工作需要统计一下文本文档中的各种不同类字符的数量。将txt文本文档中包含的的中文、英文、数字等字符数量进行统计。这当然可以使用py
- kNN(k-nearest neighbor)是一种基本的分类与回归的算法。这里我们先只讨论分类中的kNN算法。k邻近算法的输入为实例的特征
- 关于使用CTE(公用表表达式)的递归查询----SQL Server 2005及以上版本公用表表达式 (CTE) 具有一个重要的优点,那就是
- 以前在一个图书类网站看到这样一个功能:客户可以按条件搜索书目的信息,服务器会将符合条件的信息筛选出来保存为一个Excel文件供客户下载。今天
- 下面给大家分享python 字符串string的内置方法,具体内容详情如下所示:#__author: "Pizer Wang&qu
- JavaScript闭包,是JS开发工程师必须深入了解的知识。3月份自己曾撰写博客《JavaScript闭包》,博客中只是简单阐述了闭包的工
- 前言numpy.linalg模块包含线性代数的函数。使用这个模块,可以计算逆矩阵、求特征值、解线性方程组以及求解行列式等。本文讲给大家介绍关
- 今天发现一个很好用二维数组排序的php方法,usort,推荐给大家,以后二维数组里面,要按照一个字段的值排序用这个方法简单高效,例如下面的数
- 1.配置环境操作系统:Ubuntu20.04CUDA版本:11.4Pytorch版本:1.9.0TorchVision版本:0.7.0IDE
- 一、前言MYSQL中MDL锁一直是一个比较让人比较头疼的问题,我们谈起锁一般更加倾向于INNODB下层的gap lock、next key
- 一、绘图命令操纵海龟绘图有很多命令,可以划分为三种:画笔运动命令、画笔控制命令、全局控制命令1、画笔运动命令命令说明turtle.forwa
- 方法一:def printTheReverseArray(self): list_1 = [1, 2, 3, 4, 5, 6, 7] l
- 本文实例讲述了PHP函数shuffle()取数组若干个随机元素的方法。分享给大家供大家参考,具体如下:有时候我们需要取数组中若干个随机元素(
- 目录函数什么是函数/方法2.为什么需要函数1、载体2、组织3、复用4、封装5、清晰6、按需3.如何声明/调用一个函数4.函数/方法的参数1、
- 为什么会有多个分支一般项目在开发阶段,都会创建多个分支,用于不同开发阶段的版本发布如:master、dev等,之所以会有这种多分支情况,就是
- 例表:假如想要去掉表中的‘#',‘;'而且以‘#'和‘;'为分割线切割数据:#将dfxA_2的每一个分隔符之
- Cookie 模块,顾名思义,就是用来操作Cookie的模块。Cookie这块小蛋糕,玩过Web的人都知道,它是Server与Client保
- 目录range函数的使用第一种创建方式第二种创建方式第三种创建方式判断指定的数有没有在当前序列中循环结构总结range函数的使用作为循环遍历
- 不通过数据源名DSN也能访问Access数据库吗?代码如下:<% dim conn &nbs