一个commit引发的思考
这几天我翻了翻golang的提交记录,发现了一条很有意思的提交:bc593ea,这个提交看似简单,但是引人深思。
commit讲了什么
commit的标题是“sync: document implementation of Once.Do”,显然是对文档做些补充,然而奇怪的是为什么要对某个功能的实现做文档说明呢,难道不是配合代码+注释就能理解的吗?
根据commit的描述我们得知,Once.Do的实现问题在过去几个月内被问了至少两次,所以官方决定澄清:
It's not correct to use atomic.CompareAndSwap to implement Once.Do,
and we don't, but why we don't is a question that has come up
twice on golang-dev in the past few months.
Add a comment to help others with the same question.
不过这不是这个commit的精髓,真正有趣的部分是添加的那几行注释。
有趣的疑问
commit添加的内容如下:
乍一看可能平平无奇,然而仔细思考过后,我们就会发现问题了。
众所周知,sync.Once
用于保证某个操作只会执行一次,因此我们首先考虑到的就是为了并发安全加mutex,但是once对性能有一定要求,所以我们选用原子操作。
这时候atomic.CompareAndSwapUint32
很自然的就会浮现在脑海里,而下面的结构也很自然的就给出了:
func (o *Once) Do(f func()) {
if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
f()
}
}
然而正是这种自然联想的方案却是官方否定的,为什么?
原因很简单,举个例子,我们有一个模块,使用模块里的方法前需要初始化,否则会报错:
module.go:
package module
var flag = true
func InitModule() {
// 这个初始化模块的方法不可以调用两次以上,以便于结合sync.Once使用
if !flag {
panic("call InitModule twice")
}
flag = false
}
func F() {
if flag {
panic("call F without InitModule")
}
}
main.go:
package main
import (
"module"
"sync"
"time"
)
var o = &sync.Once{}
func DoSomeWork() {
o.Do(module.InitModule()) // 不能多次初始化,所以要用once
module.F()
}
func main() {
go DoSomeWork() // goroutine1
go DoSomeWork() // goroutine2
time.Sleep(time.Second * 10)
}
现在不管goroutine1还是goroutine2后运行,module
都能被正确初始化,对于F
的调用也不会panic,但我们不能忽略一种更常见的情况:两个goroutine同时运行会发生什么?
我们列举其中一种情况:
- goroutine1先运行,这时如果按我们所想的once实现,CAS操作成功,
InitModule
开始执行 - 这时goroutine2也在运行,但CAS因为别的routine操作成功,这里返回失败,
InitModule
执行被跳过 - Once.Do返回就意味着我们需要的操作已经被执行,这时goroutine2开始执行
F()
- 但是我们的
InitModule
在goroutine1中因为某些原因没执行完,所以我们不能调用F
- 于是问题发生了
你可能已经看出问题了,我们没有等到被调用函数执行完就返回了,导致了其他goroutine获得了一个不完整的初始化状态。
解决起来也很简单:
- 我们先判断执行标志,如果已经执行过就直接返回
- 因为是判断执行标志而不修改,就会有多个routine同时判断位true的情况,我们用mutex原子化对被调用函数
f
的操作 - 获得mutex之后先检查执行标志,以免重复执行
- 接着调用
f
- 然后我们把执行标志设置为1
- 最后解除mutex,当其他进入判断的routine重复上述过程时就能保证
f
只会被调用一次了
这是代码:
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
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()
}
}
结束语
从这个问题我们可以看到,并发编程其实并不难,我们给出的解决方案是相当简单的,然而难的在于如何全面的思考并发中会遇到的问题从而编写并发安全的代码。
golang的这个commit给了我们一个很好的例子,同时也是一个很好的启发。
一个commit引发的思考的更多相关文章
- Spring之LoadTimeWeaver——一个需求引发的思考---转
原文地址:http://www.myexception.cn/software-architecture-design/602651.html Spring之LoadTimeWeaver——一个需求引 ...
- 由一个emoji引发的思考
由一个emoji引发的思考 从毕业以来,基本就一直在做移动端,但是一直就关于移动端的开发,各种适配问题的解决,在日常搬砖中处理了就过了,也没有把东西都沉淀下来,觉得甚是寒颜.现就一个小bug,让我们来 ...
- 从一个聊天信息引发的思考之Android事件分发机制
转载请声明:http://www.cnblogs.com/courtier/p/4295235.html 起源: 我在某一天看到了下面的一条信息(如下图),我想了下(当然不是这 ...
- vmware中如何检查cpu的使用状况-一个考题引发的思考
来自一个VCP的考题,有点兴趣.可参看: 如何在VMware里使用esxtop? http://thocm.com/a/caozuoxitongzixun/xunihuazonghezixun/VMw ...
- MyBatis 学习记录7 一个Bug引发的思考
主题 这次学习MyBatis的主题我想记录一个使用起来可能会遇到,但是没有经验的话很不好解决的BUG,在特定情况下很容易发生. 异常 java.lang.IllegalArgumentExceptio ...
- 一个ScheduledExecutorService启动的Java线程无故挂掉引发的思考
2018年12月12日18:44:53 一个ScheduledExecutorService启动的Java线程无故挂掉引发的思考 案件现场 不久前,在开发改造公司一个端到端监控日志系统的时候,出现了一 ...
- 一个小BUG引发的思考。(论开发与测试之间的那点事)
标题不是“一个馒头引发的血案”. 言归正传:今天上午测试的时候,发现了一个BUG,如图: 一个用肉眼就能发现的BUG.原因当然是因为开发同事没有自测试,流入到了测试人员这里了. 无非是开发同事不严谨造 ...
- SQLAlchemy并发写入引发的思考
背景 近期公司项目中加了一个积分机制,用户登录签到会获取登录积分,但会出现一种现象就是用户登录时会增加双倍积分,然后生成两个积分记录.此为问题 问题分析 项目采用微服务架构,下图为积分机制流程 ...
- 由SecureCRT引发的思考和学习
由SecureCRT引发的思考和学习 http://mp.weixin.qq.com/s?__biz=MzAxOTAzMDEwMA==&mid=2652500597&idx=1& ...
随机推荐
- OpenGL(十四) 模板测试
启用模板测试时,OpenGL会在内存中开辟一块空间作为模板缓冲区,里边保存了每个像素的"模板值",模板测试的过程就是把每一个像素的模板值与一个设定的模板参考值进行比较,符合设定条件 ...
- GTK+浅谈之一Windows10下QtCreator中GTK+环境搭建(十几篇)
一.简介 虽然GTK+是Linux下的开发环境,因为其跨平台特性,有时候需要在Windows上用到它的.如下是在Windows10下配置GTK+的开发环境. Gnome的开发基础结构是围 ...
- Oracle 已有则更新,没有则插入
使用merge merge into 表名 t1 using (select '数据数据' 字段1,'数据数据' 字段2 from dual) t2 on (t1.字段1 = t2.字段1) when ...
- HALCON学习之算子大全
1.1 Gaussian-Mixture-Models 1.add_sample_class_gmm 功能:把一个训练样本添加到一个高斯混合模型的训练数据上. 2.classify_class_gmm ...
- byte[] 左移和右移
public static class ex { public static byte[] RightShift(this byte[] ba, int n) { ) { return ba.Left ...
- PHP 实现自动加载器(Autoloader)
我们知道PHP可以实现自动加载,避免了繁重的体力活,代码更规范,整洁.那如果我们把这个自动加载再升华一下,变成自动加载类,每次只需要引入这个类,那么其他类就自动加载了,已经开源,仓库地址在这里.同时如 ...
- Win8 Metro(C#)数字图像处理--2.50图像运动模糊
原文:Win8 Metro(C#)数字图像处理--2.50图像运动模糊 [函数名称] 图像运动模糊算法 MotionblurProcess(WriteableBitmap src,int ...
- 活锁(livelock) 专题
活锁(livelock) 活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败. 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于 ...
- SQLServer 进程无法向表进行大容量复制(错误号: 22018 20253)
原文:SQLServer 进程无法向表进行大容量复制 我的环境:SQL SERVER 2008 R2:发布者 ->SQL SERVER 2017 订阅者 进程无法向表“"dbo&quo ...
- 读unp并动手实践
经过三个月的学习,我发现进度比较慢.照这个进度下去,平均一周花费5-6小时,还不知道读完全书需要多久. 现在做个计划,全书除开简介部分分为 基础 和 高级 套接字编程两部分,其中 基础可以分为 TCP ...