这几天我翻了翻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同时运行会发生什么?

我们列举其中一种情况:

  1. goroutine1先运行,这时如果按我们所想的once实现,CAS操作成功,InitModule开始执行
  2. 这时goroutine2也在运行,但CAS因为别的routine操作成功,这里返回失败,InitModule执行被跳过
  3. Once.Do返回就意味着我们需要的操作已经被执行,这时goroutine2开始执行F()
  4. 但是我们的InitModule在goroutine1中因为某些原因没执行完,所以我们不能调用F
  5. 于是问题发生了

你可能已经看出问题了,我们没有等到被调用函数执行完就返回了,导致了其他goroutine获得了一个不完整的初始化状态。

解决起来也很简单:

  1. 我们先判断执行标志,如果已经执行过就直接返回
  2. 因为是判断执行标志而不修改,就会有多个routine同时判断位true的情况,我们用mutex原子化对被调用函数f的操作
  3. 获得mutex之后先检查执行标志,以免重复执行
  4. 接着调用f
  5. 然后我们把执行标志设置为1
  6. 最后解除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引发的思考的更多相关文章

  1. Spring之LoadTimeWeaver——一个需求引发的思考---转

    原文地址:http://www.myexception.cn/software-architecture-design/602651.html Spring之LoadTimeWeaver——一个需求引 ...

  2. 由一个emoji引发的思考

    由一个emoji引发的思考 从毕业以来,基本就一直在做移动端,但是一直就关于移动端的开发,各种适配问题的解决,在日常搬砖中处理了就过了,也没有把东西都沉淀下来,觉得甚是寒颜.现就一个小bug,让我们来 ...

  3. 从一个聊天信息引发的思考之Android事件分发机制

         转载请声明:http://www.cnblogs.com/courtier/p/4295235.html 起源:        我在某一天看到了下面的一条信息(如下图),我想了下(当然不是这 ...

  4. vmware中如何检查cpu的使用状况-一个考题引发的思考

    来自一个VCP的考题,有点兴趣.可参看: 如何在VMware里使用esxtop? http://thocm.com/a/caozuoxitongzixun/xunihuazonghezixun/VMw ...

  5. MyBatis 学习记录7 一个Bug引发的思考

    主题 这次学习MyBatis的主题我想记录一个使用起来可能会遇到,但是没有经验的话很不好解决的BUG,在特定情况下很容易发生. 异常 java.lang.IllegalArgumentExceptio ...

  6. 一个ScheduledExecutorService启动的Java线程无故挂掉引发的思考

    2018年12月12日18:44:53 一个ScheduledExecutorService启动的Java线程无故挂掉引发的思考 案件现场 不久前,在开发改造公司一个端到端监控日志系统的时候,出现了一 ...

  7. 一个小BUG引发的思考。(论开发与测试之间的那点事)

    标题不是“一个馒头引发的血案”. 言归正传:今天上午测试的时候,发现了一个BUG,如图: 一个用肉眼就能发现的BUG.原因当然是因为开发同事没有自测试,流入到了测试人员这里了. 无非是开发同事不严谨造 ...

  8. SQLAlchemy并发写入引发的思考

    背景 近期公司项目中加了一个积分机制,用户登录签到会获取登录积分,但会出现一种现象就是用户登录时会增加双倍积分,然后生成两个积分记录.此为问题  问题分析 项目采用微服务架构,下图为积分机制流程   ...

  9. 由SecureCRT引发的思考和学习

    由SecureCRT引发的思考和学习 http://mp.weixin.qq.com/s?__biz=MzAxOTAzMDEwMA==&mid=2652500597&idx=1& ...

随机推荐

  1. Android学习路径(22)应用Fragment建立动态UI——构建一个灵活UI

    当你设计你的应用来支持多个屏幕尺寸.你能够基于可用的屏幕空间通过在不同的布局上重用fragment来优化用户体验. 比如,在一个手机上.使用单面板(一次仅仅显示一个fragment)的用户体验更加合适 ...

  2. 不使用运算符(+、-、*、/) 来进行四则运算(C#)

    最近在LeetCode 上刷题,遇到一个非常有趣的题目,题目的大概意思就是在不使用运算符的情况下实现两个数的加法...原题点这里>>> 说实话,刚看到这题目,我是一脸懵逼的. 后来仔 ...

  3. HDU4421 Bit Magic 【2-sat】

    叙述性说明: 这给出了一个矩阵,原来的请求a排列 2-sat称号.对于每一位跑步边,跑31位可 详细的施工方 注意N=1的情况特判,还有检查对称元素是否同样 #include <stdio.h& ...

  4. debian9 安装 odoo11 笔记用 部分内容转载前辈的,在此感谢

    1先创建个odoo用户 sudo adduser odoo 2:给root 权限: sudo vi /etc/sudoers 修改文件参考如下: # User privilege specificat ...

  5. WPF Binding的代码实现

    using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threa ...

  6. wpf怎么使用WindowsFormsHost(即winform控件)

    原文:wpf怎么使用WindowsFormsHost(即winform控件) 使用方法: 1.首先,我们需要向项目中的引用(reference)中添加两个动态库dll,一个是.NET库中的System ...

  7. vxworks下libpcap的移植

    linux下的libpcap应用能够成熟的使用在第三方的应用中,但基于vxworks开发的项目中需要使用libpcap的部分功能则无相应的实现. 研究了下libpcap向vxworks的移植,并且小有 ...

  8. linux安装脚本

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 3 ...

  9. C# 获取当前月份天数的三种方法总结

    方法一: //最有含量的一种 int days = System.Threading.Thread.CurrentThread.CurrentUICulture.Calendar.GetDaysInM ...

  10. Android零基础入门第64节:揭开RecyclerView庐山真面目

    原文:Android零基础入门第64节:揭开RecyclerView庐山真面目 大家还记得之前在第38期~第50期都在学习列表控件吗,其中用了8期讲ListView的使用,相信都已经掌握好了吧.那么本 ...