sync.Once 的前世今生
大家好,我是好久不见的薯条,上篇文章 编写一个配置化的Kafka Proxy,让你分钟级别接入Kafka的阅读量很惨淡,搞得我那段时间有点丧,可能大家还是更喜欢Golang方面的文章,也可能是那篇写的有点搓... 这几天北京降温又下雨,我久违的感冒了,秋高气爽,读者朋友们要注意多加衣服啊,感冒还是很难受的。
这篇once的文章前前后后看了好多参考,改了好几遍,最终出来这么个鸟样子,个人感觉并发编程这块水很深,因为这块不仅涉及Golang源码,还涉及到汇编、操作系统、甚至是硬件的知识,真是学无止境,有兴趣的朋友可以查一下Read Acquire
、Write Release
和Golang官方的Memory Model一文。
以下是正文:
type Resource struct {addr string
}var Res *Resource
var once sync.Oncefunc GetResourceOnce(add string) *Resource {once.Do(func() {Res = &Resource{addr: add}})return Res
}func main() {fmt.Println(GetResource("beijing"))
}
// output:{beijing}
例子:
var Resp *Resource
var mut sync.Mutexfunc GetResourceMutex(add string) *Resource {mut.Lock()defer mut.Unlock()if Resp != nil {return Resp}Resp = &Resource{addr: add}return Resp
}
1. 为啥源码引入Mutex而不是CAS操作
3. 为啥要有fast path, slow path
4. 加锁之后为啥要有done==0,为啥有double check,为啥这里不是原子读
4.store为啥要加defer
5.为啥是atomic.store,不是直接赋值1
Once开始的地方
type Once struct {m Mutexdone bool
}func (o *Once) Do(f func()) {o.m.Lock()defer o.m.Unlock()if !o.done {o.done = truef()}
}
在这段2010年8月15日提交的代码中,作者借助Mutex
实现Once语义,执行的时候先加一把互斥锁,保证只有一个协程可以操作done
变量,等f
函数执行完解锁。
这样的代码相当于mvp版本,管用,但是略显粗糙,一个最显而易见的缺点:每次都要执行Mutex加锁操作,对于Once这种语义有必要吗,是否可以先判断一下done的value是否为true,然后再进行加锁操作呢?
第一次进化
于是Once开始了第一次进化,这次优化改进了上面提到的问题:若Once已经初始化,那么Do内部将不会执行抢锁操作。做这份代码改动的哥们经过测试发现这样改在不同核的benchmark中有92%-99%的耗时提升。
type Once struct {m Mutexdone 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语义。
TEXT runtime∕internal∕atomic·Store(SB), NOSPLIT, $0-12MOVQ ptr+0(FP), BXMOVL val+8(FP), AXXCHGL AX, 0(BX)RET
小优化三
这次的优化在StoreUint32
前增加defer前缀,增加defer是保证 即使f()
在执行过程中出现panic,Once仍然保证f()
只执行一次,这样符合严格的Once语义。
除了预防panic,defer还能解决指令重排的问题:现在CPU为了执行效率,源码在真正执行时的顺序和代码的顺序可能并不一样,比如这段代码中a不一定打印"hello, world",也可能打印空字符串。
var a string
var done boolfunc 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%
的提升。
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)
}
sync.Once 的前世今生相关推荐
- 时势下的HMS和GMS的前世今生——前生篇
致各位互联网安卓应用开发者的一封信: 我是大菊厂消费者BG HMS云服务安全工程部运营喵一枚,学名"安全运营喵酱",受邀在开发者社区论坛HMS Core发布自己对HMS VS ...
- (一)什么是1588?1588的前世今生?1588的时间同步原理
五年之前,曾经做过1588,近日在回顾的时候,发现针对于1588的成体系的资料仍是比较少,虽说协议的介绍确实是够全面,但是针对于初次接触协议的人而言,学习的陡峭的程度还是有的,门槛还是有的.对于初学者 ...
- 郭健: Linux进程调度技术的前世今生之“前世”
作者简介 郭健,一名普通的内核工程师,以钻研Linux内核代码为乐,热衷于技术分享,和朋友一起创建了蜗窝科技的网站,希望能汇集有同样想法的技术人,以蜗牛的心态探讨技术. (小编画外音:郭大侠是我最佩服 ...
- Go 学习笔记(67)— Go 并发安全字典 sync.Map
1. 并发不安全的 map Go 语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的. 换句话说,在同一时间段内,让不同 goroutine 中的代码,对同一个字典进行读写操作是 ...
- Go 学习笔记(66)— Go 并发同步原语(sync.Mutex、sync.RWMutex、sync.Once)
1. 竞态条件 一旦数据被多个线程共享,那么就很可能会产生争用和冲突的情况.这种情况也被称为竞态条件(race condition),这往往会破坏共享数据的一致性. 举个例子,同时有多个线程连续向同一 ...
- Error: Gradle project sync failed. Please fix your project and try again.
下载一个demo 显示是这样的 这样问题的处理方法 1 首先检查下gradle 是否下载了 如果出现下面文字提示是没有下载了,可能是网络的问题引起的问题 Gradle sync failed: Co ...
- Go 学习笔记(23)— 并发(02)[竞争,锁资源,原子函数sync/atomic、互斥锁sync.Mutex]
本文参考 <Go 语言实战> 1. 竞争状态简述 如果两个或者多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况 ...
- linux系统安装deamonsync,DAEMON Sync的使用心得体会。简易版家庭云服务器!正是我要的那种...
作为一个超级菜鸟,今天学会了自己建立一个简易的局域网云盘,以后就可以让家里人同步照片进电脑了!我的需求仅仅如此而已! (1)保证所有设备在同一局域网内.(我现在的理解就是在同一个路由器内,不知道是不是 ...
- android jar 电子书下载,【Android】Gradle project sync jar包长时间下载不下来的解决办法...
当我们新建一个Android项目,或者在项目中依赖使用一个新的第三方库时,Android Studio经常会从jcenter或者maven仓库下载jar包,但是我们的网络环境不一定一直那么的顺畅,当网 ...
最新文章
- 数字化转型 用数据重塑未来业务
- 转:ECharts图表组件之简单关系图:如何轻松实现另类站点地图且扩展节点属性实现点击节点页面跳转...
- c语言双引号和单引号的区别
- 链表之判断一个链表是否为回文结构(三)
- python交互式绘图比较_python – 基于Tkinter和matplotlib的交互式绘图
- Trie 树内存消耗问题
- C# WPF ASP.net 上传多文件和数据
- 字符串常量池、堆、栈
- 导出excel文件,若在一台PC端无法正常导出,闪退,进程还存在时(以下方法也许对你有用)
- erf函数处以一个常数_Google对Linux专利处以500万美元的罚款
- 学习了金字塔原理中的SCQA分析法
- 米家扫地机器人静音模式在哪_米家扫地机器人2代发布日期确定:带拖地功能...
- 时间管理 android app推荐,干货星球 篇十三:【强烈安利】分享10个时间管理APP,每一个都堪称精品!...
- 数值分析常见算法C++实现
- opencv-11-中值滤波及自适应中值滤波
- c语言求100以内偶数和while,c语言 求1到100以内的偶数之和
- FileZilla远程上传文件失败原因和解决办法
- IDE工具(27) idea点击箭头快速切换到相关联的类位置 (Free MyBatis插件)
- List的toArray()方法和toArray(T[] a)方法
- 财务内部收益率用计算机怎么算,财务内部收益率EXCEL怎么计算
热门文章
- JAVA ++ 运算符题目
- 2020-09-26
- 用python写网络爬虫 -从零开始 3 编写ID遍历爬虫
- ubuntu下安装chromium及其pepperflash
- android 官方教程中文版
- HDU - 6184 Counting Stars(思维+三元环)
- ZOJ - 3228 Searching the String(AC自动机求不重复子串出现次数)
- UVA - 12338 Anti-Rhyme Pairs(后缀数组)
- 20000字节的包算大吗_20000的包和200的包区别,戳中了万千女人的内心!
- 0001-Two Sum(两数之和)