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

这篇once的文章前前后后看了好多参考,改了好几遍,最终出来这么个鸟样子,个人感觉并发编程这块水很深,因为这块不仅涉及Golang源码,还涉及到汇编、操作系统、甚至是硬件的知识,真是学无止境,有兴趣的朋友可以查一下Read AcquireWrite 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 的前世今生相关推荐

  1. 时势下的HMS和GMS的前世今生——前生篇

    致各位互联网安卓应用开发者的一封信:   我是大菊厂消费者BG HMS云服务安全工程部运营喵一枚,学名"安全运营喵酱",受邀在开发者社区论坛HMS Core发布自己对HMS VS ...

  2. (一)什么是1588?1588的前世今生?1588的时间同步原理

    五年之前,曾经做过1588,近日在回顾的时候,发现针对于1588的成体系的资料仍是比较少,虽说协议的介绍确实是够全面,但是针对于初次接触协议的人而言,学习的陡峭的程度还是有的,门槛还是有的.对于初学者 ...

  3. 郭健: Linux进程调度技术的前世今生之“前世”

    作者简介 郭健,一名普通的内核工程师,以钻研Linux内核代码为乐,热衷于技术分享,和朋友一起创建了蜗窝科技的网站,希望能汇集有同样想法的技术人,以蜗牛的心态探讨技术. (小编画外音:郭大侠是我最佩服 ...

  4. Go 学习笔记(67)— Go 并发安全字典 sync.Map

    1. 并发不安全的 map Go 语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的. 换句话说,在同一时间段内,让不同 goroutine 中的代码,对同一个字典进行读写操作是 ...

  5. Go 学习笔记(66)— Go 并发同步原语(sync.Mutex、sync.RWMutex、sync.Once)

    1. 竞态条件 一旦数据被多个线程共享,那么就很可能会产生争用和冲突的情况.这种情况也被称为竞态条件(race condition),这往往会破坏共享数据的一致性. 举个例子,同时有多个线程连续向同一 ...

  6. Error: Gradle project sync failed. Please fix your project and try again.

    下载一个demo  显示是这样的 这样问题的处理方法 1 首先检查下gradle 是否下载了 如果出现下面文字提示是没有下载了,可能是网络的问题引起的问题 Gradle sync failed: Co ...

  7. Go 学习笔记(23)— 并发(02)[竞争,锁资源,原子函数sync/atomic、互斥锁sync.Mutex]

    本文参考 <Go 语言实战> 1. 竞争状态简述 如果两个或者多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况 ...

  8. linux系统安装deamonsync,DAEMON Sync的使用心得体会。简易版家庭云服务器!正是我要的那种...

    作为一个超级菜鸟,今天学会了自己建立一个简易的局域网云盘,以后就可以让家里人同步照片进电脑了!我的需求仅仅如此而已! (1)保证所有设备在同一局域网内.(我现在的理解就是在同一个路由器内,不知道是不是 ...

  9. android jar 电子书下载,【Android】Gradle project sync jar包长时间下载不下来的解决办法...

    当我们新建一个Android项目,或者在项目中依赖使用一个新的第三方库时,Android Studio经常会从jcenter或者maven仓库下载jar包,但是我们的网络环境不一定一直那么的顺畅,当网 ...

最新文章

  1. 数字化转型 用数据重塑未来业务
  2. 转:ECharts图表组件之简单关系图:如何轻松实现另类站点地图且扩展节点属性实现点击节点页面跳转...
  3. c语言双引号和单引号的区别
  4. 链表之判断一个链表是否为回文结构(三)
  5. python交互式绘图比较_python – 基于Tkinter和matplotlib的交互式绘图
  6. Trie 树内存消耗问题
  7. C# WPF ASP.net 上传多文件和数据
  8. 字符串常量池、堆、栈
  9. 导出excel文件,若在一台PC端无法正常导出,闪退,进程还存在时(以下方法也许对你有用)
  10. erf函数处以一个常数_Google对Linux专利处以500万美元的罚款
  11. 学习了金字塔原理中的SCQA分析法
  12. 米家扫地机器人静音模式在哪_米家扫地机器人2代发布日期确定:带拖地功能...
  13. 时间管理 android app推荐,干货星球 篇十三:【强烈安利】分享10个时间管理APP,每一个都堪称精品!...
  14. 数值分析常见算法C++实现
  15. opencv-11-中值滤波及自适应中值滤波
  16. c语言求100以内偶数和while,c语言 求1到100以内的偶数之和
  17. FileZilla远程上传文件失败原因和解决办法
  18. IDE工具(27) idea点击箭头快速切换到相关联的类位置 (Free MyBatis插件)
  19. List的toArray()方法和toArray(T[] a)方法
  20. 财务内部收益率用计算机怎么算,财务内部收益率EXCEL怎么计算

热门文章

  1. JAVA ++ 运算符题目
  2. 2020-09-26
  3. 用python写网络爬虫 -从零开始 3 编写ID遍历爬虫
  4. ubuntu下安装chromium及其pepperflash
  5. android 官方教程中文版
  6. HDU - 6184 Counting Stars(思维+三元环)
  7. ZOJ - 3228 Searching the String(AC自动机求不重复子串出现次数)
  8. UVA - 12338 Anti-Rhyme Pairs(后缀数组)
  9. 20000字节的包算大吗_20000的包和200的包区别,戳中了万千女人的内心!
  10. 0001-Two Sum(两数之和)