Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Sync Once:不是吧,不到20行源码居然来回改了这么多次

Sync Once:不是吧,不到20行源码居然来回改了这么多次

作者头像
薯条的编程修养
发布于 2022-08-10 11:40:13
发布于 2022-08-10 11:40:13
23700
代码可运行
举报
运行总次数:0
代码可运行

大家好,我是好久不见的薯条,上篇文章 编写一个配置化的Kafka Proxy,让你分钟级别接入Kafka 的阅读量很惨淡,搞得我那段时间有点丧,可能大家还是更喜欢Golang方面的文章,也可能是那篇写的有点搓... 这几天北京降温又下雨,我久违的感冒了,秋高气爽,读者朋友们要注意多加衣服啊,感冒还是很难受的。

这篇once的文章前前后后看了好多参考,改了好几遍,最终出来这么个鸟样子,个人感觉并发编程这块水很深,因为这块不仅涉及Golang源码,还涉及到汇编、操作系统、甚至是硬件的知识,真是学无止境,有兴趣的朋友可以查一下Read AcquireWrite Release和Golang官方的Memory Model一文。

以下是正文:


代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type Resource struct {
 addr string
}

var Res *Resource
var once sync.Once

func GetResourceOnce(add string) *Resource {

 once.Do(func() {
  Res = &Resource{addr: add}
 })

 return Res
}

func main() {
 fmt.Println(GetResource("beijing"))
}
// output:{beijing}

例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var Resp *Resource
var mut sync.Mutex

func GetResourceMutex(add string) *Resource {
 mut.Lock()
 defer mut.Unlock()

 if Resp != nil {
  return Resp
 }

 Resp = &Resource{addr: add}

 return Resp
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1. 为啥源码引入Mutex而不是CAS操作
3. 为啥要有fast path, slow path
4. 加锁之后为啥要有done==0,为啥有double check,为啥这里不是原子读
4.store为啥要加defer
5.为啥是atomic.store,不是直接赋值1

Once开始的地方

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type Once struct {
 m    Mutex
 done bool
}

func (o *Once) Do(f func()) {
 o.m.Lock()
 defer o.m.Unlock()
 if !o.done {
  o.done = true
  f()
 }
}

在这段2010年8月15日提交的代码中,作者借助Mutex实现Once语义,执行的时候先加一把互斥锁,保证只有一个协程可以操作done变量,等f函数执行完解锁。

这样的代码相当于mvp版本,管用,但是略显粗糙,一个最显而易见的缺点:每次都要执行Mutex加锁操作,对于Once这种语义有必要吗,是否可以先判断一下done的value是否为true,然后再进行加锁操作呢?

第一次进化

于是Once开始了第一次进化,这次优化改进了上面提到的问题:若Once已经初始化,那么Do内部将不会执行抢锁操作。做这份代码改动的哥们经过测试发现这样改在不同核的benchmark中有92%-99%的耗时提升。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type Once struct {
 m    Mutex
 done int32
}

func (o *Once) Do(f func()) {
 if atomic.AddInt32(&o.done, 0) == 1 {
  return
 }
 // Slow-path.
 o.m.Lock()
 defer o.m.Unlock()
 if o.done == 0 {
  f()
  atomic.CompareAndSwapInt32(&o.done, 0, 1)
 }
}

在这段代码中,在slow-path加锁后,要继续判断done值是否为0,确认done为0后才要执行f()函数,这是因为在多协程环境下仅仅通过一次atomic.AddInt32判断并不能保证原子性,比如俩协程g1、g2,g2在g1刚刚执行完atomic.CompareAndSwapInt32(&o.done, 0, 1)进入了slow path,如果不进行double check,那g2又会执行一次f()

在这次改动中,作者用一个int32变量done表示once的对象是否已执行完,有两个地方使用到了atomic包里的方法对o.done进行判断,分别是,用AddInt32函数根据o.done的值是否为1判断once是否已执行过,若执行过直接返回;f()函数执行完后,对o.done通过cas操作进行赋值1。

这两处地方的存在有一定的争议性,在源码cr的过程中就被问到atomic.CompareAndSwapInt32(&o.done, 0, 1)可否被o.done == 1替换, 答案是不可以。

现在的CPU一般拥有多个核心,而CPU的处理速度快于从内存读取变量的速度,为了弥补这俩速度的差异,现在CPU每个核心都有自己的L1、L2、L3级高速缓存,CPU可以直接从高速缓存中读取数据,但是这样一来内存中的一份数据就在缓存中有多份副本,在同一时间下这些副本中的可能会不一样,为了保持缓存一致性,Intel CPU使用了MESI协议。

AddInt32方法和CompareAndSwapInt32方法(均为amd64平台 runtime/internal/atomic/atomic_amd64.s)底层都是在汇编层面调用了LOCK指令,LOCK指令通过总线锁或MESI协议保证原子性(具体措施与CPU的版本有关),提供了强一致性的缓存读写保证,保证LOCK之后的指令在带LOCK前缀的指令执行之后才执行,从而保证读到最新的o.done值。

第二次进化

至此Once的代码已经成型了,后面来列举一些小优化的集合:

小优化一

这个小优化把done的类型由int32替换为uint32,用CompareAndSwapUint32替换了CompareAndSwapInt32, 用LoadUint32替换了AddInt32方法,LoadUint32底层并没有LOCK指令用于加锁,我觉得能这么写的主要原因是进入slow path之后会继续用Mutex加锁并判断o.done的值,且后面的CAS操作是加锁的,所以可以这么改。这次优化经过benchmark测试性能在不同核心上有45%-94%的提升。

小优化二

这次小优化用StoreUint32替换了CompareAndSwapUint32操作,CAS操作在这里确实有点多余,因为这行代码最主要的功能是原子性的done = 1

Store命令的底层是,其中关键的指令是XCHG,有的同学可能要问了,这源码里没有LOCK指令啊,怎么保证happen before呢,Intel手册有这样的描述: The LOCK prefix is automatically assumed for XCHG instruction.,这个指令默认带LOCK前缀,能保证Happen Before语义。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
TEXT runtime∕internal∕atomic·Store(SB), NOSPLIT, $0-12
 MOVQ ptr+0(FP), BX
 MOVL val+8(FP), AX
 XCHGL AX, 0(BX)
 RET

小优化三

这次的优化在StoreUint32前增加defer前缀,增加defer是保证 即使f()在执行过程中出现panic,Once仍然保证f()只执行一次,这样符合严格的Once语义。

除了预防panic,defer还能解决指令重排的问题:现在CPU为了执行效率,源码在真正执行时的顺序和代码的顺序可能并不一样,比如这段代码中a不一定打印"hello, world",也可能打印空字符串。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var a string
var done bool

func setup() {
 a = "hello, world"
 done = true
}

func main() {
 go setup()
 for !done {
 }
 print(a)
}

而增加了defer前缀,能保证,即使出现指令重排,done变量也能在f()函数执行完后才进行store操作。

小优化四

这次优化主要是用函数区分开了fast path和slow path,对fast path做了内联优化。这样进一步降低了使用Once的开销,因为fast path会被内联到使用once的函数调用中,每次调用的时候如果只走到fast path那么连函数调用的开销都省去了,这次优化在不同核的环境下又有54%-67%的提升。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type St struct {
 ponce *sync.Once
}

func (st *St) Reset() {
 st.ponce = new(sync.Once)
}

func main() {
 s := &St{}

 f1 := func() {
  fmt.Println("hello, world")
 }
 s.Reset()
 s.ponce.Do(f1)
 s.Reset()
 s.ponce.Do(f1)
}
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-09-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 薯条的编程修养 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
浅谈golang中的sync包
Mutex其实就是一种互斥锁,Mutex一般叫做写锁,即不管读写都会锁住;RWMutex一般叫做读写锁,只有写时才会锁住,读时不会锁住,常用于读多写少的场景,就是为了解决Mutex不管读写都加锁的特性。
素履coder
2022/09/28
6020
浅谈golang中的sync包
golang源码分析(5):sync.Once
sync.once可以控制函数只能被调用一次,不能多次重复调用。 我们可以用下面的代码实现一个线程安全的单例模式 package singleton import ( "fmt" "sync" ) type object struct { name string } var once sync.Once var obj *object //单例指针 //公开方法 外包调用 func Instance() *object { once.Do(getObj) re
golangLeetcode
2022/08/02
1860
Golang 基础:底层并发原语 Mutex RWMutex Cond WaitGroup Once等使用和基本实现
上一篇 《原生并发 goroutine channel 和 select 常见使用场景》 介绍了基于 CSP 模型的并发方式。
张拭心 shixinzhang
2022/05/10
4060
Golang 基础:底层并发原语 Mutex RWMutex Cond WaitGroup Once等使用和基本实现
你真的了解 sync.Mutex吗
Mutex是一个互斥的排他锁,零值Mutex为未上锁状态,Mutex一旦被使用 禁止被拷贝。使用起来也比较简单
用户3904122
2022/06/29
3900
你真的了解 sync.Mutex吗
Go sync.Once:简约而不简单的并发利器
在某些场景下,我们需要初始化一些资源,例如单例对象、配置等。实现资源的初始化有多种方法,如定义 package 级别的变量、在 init 函数中进行初始化,或者在 main 函数中进行初始化。这三种方式都能确保并发安全,并在程序启动时完成资源的初始化。
陈明勇
2023/04/24
1.1K0
Go sync.Once:简约而不简单的并发利器
[Golang] 初探之 sync.Once
Once 官方描述 Once is an object that will perform exactly one action,即 Once 是一个对象,它提供了保证某个动作只被执行一次功能,最典型的场景就是单例模式。
landv
2020/06/16
9800
手摸手Go 单例模式与sync.Once
单例模式作为一个较为常见的设计模式,他的定义也很简单,将类的实例化限制为一个单个实例。在Java的世界里,你可能需要从懒汉模式、双重检查锁模式、饿汉模式、静态内部类、枚举等方式中选择一种手动撸一遍代码,但是他们操作起来很容易一不小心就会出现bug。而在Go里,内建提供了保证操作只会被执行一次的sync.Once,操作起来及其简单。
用户3904122
2022/06/29
2030
手摸手Go 单例模式与sync.Once
【Golang】sync.Once的使用
我们写一段代码来测试一下sync.Once的功能,我们再协程中进行调用观察调用次数,执行后可以发现init只打印了一次
MaybeHC
2024/04/23
1840
golang源码分析(6):sync.Mutex sync.RWMutex
默认直接使用 sync.Mutex 或是嵌入到结构体中,state 零值代表未上锁,sema 零值也是有意义的,参考下面源码加锁与解锁逻辑,稍想下就会明白的。另外参考大胡子 dave 的关于零值的文章
golangLeetcode
2022/08/02
1.3K0
Go语言——sync.Once分析
sync.Once表示只执行一次函数。要做到这点,就需要两点: (1)计数器,统计函数执行次数; (2)线程安全,保障在多G情况下,函数仍然只执行一次,比如锁。
恋喵大鲤鱼
2019/03/06
6820
详解并发编程之sync.Once的实现(附上三道面试题)
Go语言标准库中的sync.Once可以保证go程序在运行期间的某段代码只会执行一次,作用与init类似,但是也有所不同:
Golang梦工厂
2022/07/08
4100
听说Mutex源码是出名的不好看,我不信,来试一下
Mutex需要两个变量:key表示锁的使用情况,value 为0表示锁未被持有,1表示锁被持有 且 没有等待者,n表示锁被持有,有n-1个等待者;sema表示等待队列的信号量,sema是个先进先出的队列,用来阻塞、唤醒协程。
薯条的编程修养
2022/08/10
4000
听说Mutex源码是出名的不好看,我不信,来试一下
GO 单例模式
单例模式是常用的模式之一,一般介绍的单例模式有 饿汉式 和 懒汉式 等,不管那种模式最终目的只有一个,就是只实例化一次,仅允许一个实例存在。
孤烟
2020/09/27
1.1K0
万字图解| 深入揭秘Golang锁结构:Mutex(上)
   Golang的Mutex算是在日常开发中最常见的组件了,并且关于锁的知识也是面试官最喜欢问的。    曾经在一次腾讯面试中,被面试官问得体无完肤。    虽然Golang Mutex只有短短的200多行,但是已经是一个极其丰富、精炼的组件,有极其复杂的状态控制。我已经看过很多次Mutex的源码,但是总是过段时间就会又处于懵逼状态,不得其道。分析下来,猜测是缺少“历史背景”,一上来就看到的是已经经过好几轮优化的代码,但是不清楚这么优化的背景,同时也缺少一些场景,就会导致无法理解一些设计。    其实如果我们去追溯 Mutex 的演进历史,会发现,Mutex最开始是一个非常简单的实现,简单到难以置信的地步,是Go开发者们经过了好几轮的优化才变成了现在这么一个非常复杂的数据结构,这是一个逐步完善的过程。    于是我想如果我们是设计者,我们会怎么去设计去优化一个锁的实现呢?    下面我将结合我曾经的腾讯面试经历 加上 代入“设计者”的角度出发,结合Mutex 的演进历史,去分析如何设计一个功能完备的锁。希望经过本文的分析,你也可以从零设计出属于你的「Mutex」。    友情提醒:文章很长,但是绝对值得一读。
公众号 云舒编程
2024/01/25
3524
万字图解| 深入揭秘Golang锁结构:Mutex(上)
Mutex的实现
CAS 指令将给定的值和一个内存地址中的值进行比较,如果相等,则用新值替换内存地址中的值。
用户7381369
2020/10/29
1.4K0
【Go】sync.Mutex 源码分析
互斥锁的锁状态由 state 这个 32 的结构表示,这 32 位会被分成两部分:
JuneBao
2022/10/26
2950
Go 并发编程之 Mutex
友情提示:此篇文章大约需要阅读 18分钟0秒,不足之处请多指教,感谢你的阅读。 订阅本站
Meng小羽
2020/11/23
6260
Go 并发编程之 Mutex
面试官:哥们Go语言互斥锁了解到什么程度了?
sync 包下的mutex就是互斥锁,其提供了三个公开方法:调用Lock()获得锁,调用Unlock()释放锁,在Go1.18新提供了TryLock()方法可以非阻塞式的取锁操作:
Golang梦工厂
2022/07/11
4900
面试官:哥们Go语言互斥锁了解到什么程度了?
我是怎么在golang里实现单例的
本文介绍基于sync.Once的方式来实现单例,熟练掌握这种模式,并理解其底层原理,对大部分人来讲已经完全够用了。
hugo_lei
2022/11/07
5240
go 并发编程
Go 语言在 sync 包中提供了用于同步的一些基本原语,包括常见的 sync.Mutex、sync.RWMutex、sync.WaitGroup、sync.Once。
haifeiWu
2020/07/03
7580
go 并发编程
相关推荐
浅谈golang中的sync包
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验