作者:richardyao,腾讯 CSIG 后台开发工程师

并发编程中,go 不仅仅支持传统的通过共享内存的方式来通信,更推崇通过channel来传递消息,这种新的并发编程模型会出现不同于以往的bug。从 bug 中学习,《Understanding Real-World Concurrency Bugs in Go》这篇 paper 在分析了六大开源项目并发相关的bug之后,为我们总结了go并发编程中常见的坑。别往坑里跳,编程更美妙。

在 go 中,创建 goroutine 非常简单,在函数调用前加 go 关键字,这个函数的调用就在一个单独的 goroutine 中执行了;go 支持匿名函数,让创建 goroutine 的操作更加简洁。另外,在并发编程模型上,go 不仅仅支持传统的通过共享内存的方式来通信,更推崇通过 channel 来传递消息:

Do not communicate by sharing memory; instead, share memory by communicating.

这种新的并发编程模型会带来新类型的 bug,从 bug 中学习,《Understanding Real-World Concurrency Bugs in Go》这篇 paper 在 Docker、Kubernetes、etcd、gRPC、CockroachDB、BoltDB 六大开源项目的 commit log 中搜索等关键字:

"race"、"deadlock"、"synchronization"、"concurrency"、"lock"、"mutex"、"atomic"、"compete"、"context"、"once"、"goroutine leak",找出这六大项目中并发相关的 bug,然后归类这些 bug,总结出了 go 并发编程中常见的一些坑。通过学习这些坑,可以让我们在以后的项目里防范类似的错误,或者遇到类似问题的时候可以帮助指导快速定位排查。

unbuffered channel 由于 receiver 退出导致 sender 侧 block

如下面一个 bug 的例子:

func finishReq(timeout time.Duration) ob {ch := make(chan ob)go func() {result := fn()ch <- result // block}()select {case result = <-ch:return resultcase <-time.After(timeout):return nil}
}

本意是想调用 fn()时加上超时的功能,如果 fn()在超时时间没有返回,则返回 nil。但是当超时发生的时候,针对代码中第二行创建的 ch 来说,由于已经没有 receiver 了,第 5 行将会被 block 住,导致这个 goroutine 永远不会退出。

If the capacity is zero or absent, the channel is unbuffered and communication succeeds only when both a sender and receiver are ready. Otherwise, the channel is buffered and communication succeeds without blocking if the buffer is not full (sends) or not empty (receives).

这个 bug 的修复方式也是非常的简单,把 unbuffered channel 修改成 buffered channel。

func finishReq(timeout time.Duration) ob {ch := make(chan ob, 1)go func() {result := fn()ch <- result // block}()select {case result = <-ch:return resultcase <-time.After(timeout):return nil}
}

思考:在上面的例子中,虽然这样不会 block 了,但是 channel 一直没有被关闭,channel 保持不关闭是否会导致资源的泄漏呢?

WaitGroup 误用导致阻塞

下面是一个 WaitGroup 误用导致阻塞的一个 bug 的例子:https://github.com/moby/moby/pull/25384

var group sync.WaitGroup
group.Add(len(pm.plugins))
for _, p := range pm.plugins {go func(p *plugin) {defer group.Done()}(p)group.Wait()
}

当 len(pm.plugins)大于等于 2 时,第 7 行将会被卡住,因为这个时候只启动了一个异步的 goroutine,group.Done()只会被调用一次,group.Wait()将会永久阻塞。修复如下:

var group sync.WaitGroup
group.Add(len(pm.plugins))
for _, p := range pm.plugins {go func(p *plugin) {defer group.Done()}(p)
}
group.Wait()

context 误用导致资源泄漏

如下面的代码所示:

hctx, hcancel := context.WithCancel(ctx)
if timeout > 0 {hctx, hcancel = context.WithTimeout(ctx, timeout)
}

第一行 context.WithCancel(ctx)有可能会创建一个 goroutine,来等待 ctx 是否 Done,如果 parent 的 ctx.Done()的话,cancel 掉 child 的 context。也就是说 hcancel 绑定了一定的资源,不能直接覆盖。

Canceling this context releases resources associated with it, so code should call cancel as soon as the operations running in this Context complete.

这个 bug 的修复方式是:

var hctx context.Context
var hcancel context.CancelFunc
if timeout > 0 {hctx, hcancel = context.WithTimeout(ctx, timeout)
} else {hctx, hcancel = context.WithCancel(ctx)
}

或者

hctx, hcancel := context.WithCancel(ctx)
if timeout > 0 {hcancel.Cancel()hctx, hcancel = context.WithTimeout(ctx, timeout)
}

多个 goroutine 同时读写共享变量导致的 bug

如下面的例子:

for i := 17; i <= 21; i++ { // writego func() { /* Create a new goroutine */apiVersion := fmt.Sprintf("v1.%d", i) // read}()
}

第二行中的匿名函数形成了一个闭包(closures),在闭包内部可以访问定义在外面的变量,如上面的例子中,第 1 行在写 i 这个变量,在第 3 行在读 i 这个变量。这里的关键的问题是对同一个变量的读写是在两个 goroutine 里面同时进行的,因此是不安全的。

Function literals are closures: they may refer to variables defined in a surrounding function. Those variables are then shared between the surrounding function and the function literal, and they survive as long as they are accessible.

可以修改成:

for i := 17; i <= 21; i++ { // writego func(i int) { /* Create a new goroutine */apiVersion := fmt.Sprintf("v1.%d", i) // read}(i)
}

通过passed by value的方式规避了并发读写的问题。

channel 被关闭多次引发的 bug

https://github.com/moby/moby/pull/24007/files

select {
case <-c.closed:
default:close(c.closed)
}

上面这块代码可能会被多个 goroutine 同时执行,这段代码的逻辑是,case 这个分支判断 closed 这个 channel 是否被关闭了,如果被关闭的话,就什么都不做;如果 closed 没有被关闭的话,就执行 default 分支关闭这个 channel,多个 goroutine 并发执行的时候,有可能会导致 closed 这个 channel 被关闭多次。

For a channel c, the built-in function close(c) records that no more values will be sent on the channel. It is an error if c is a receive-only channel. Sending to or closing a closed channel causes a run-time panic.

这个 bug 的修复方式是:

Once.Do(func() {close(c.closed)
})

把整个 select 语句块换成 Once.Do,保证 channel 只关闭一次。

timer 误用产生的 bug

如下面的例子:

timer := time.NewTimer(0)
if dur > 0 {timer = time.NewTimer(dur)
}
select {
case <-timer.C:
case <-ctx.Done():return nil
}

原意是想 dur 大于 0 的时候,设置 timer 超时时间,但是 timer := time.NewTimer(0)导致 timer.C 立即触发。修复后:

var timeout <-chan time.Time
if dur > 0 {timeout = time.NewTimer(dur).C
}
select {
case <-timeout:
case <-ctx.Done():return nil
}

A nil channel is never ready for communication.

上面的代码中第一个 case 分支 timeout 有可能是个 nil 的 channel,select 在 nil 的 channel 上,这个分支不会被触发,因此不会有问题。

读写锁误用引发的 bug

go 语言中的 RWMutex,write lock 有更高的优先级:

If a goroutine holds a RWMutex for reading and another goroutine might call Lock, no goroutine should expect to be able to acquire a read lock until the initial read lock is released. In particular, this prohibits recursive read locking. This is to ensure that the lock eventually becomes available; a blocked Lock call excludes new readers from acquiring the lock.

如果一个 goroutine 拿到了一个 read lock,然后另外一个 goroutine 调用了 Lock,第一个 goroutine 再调用 read lock 的时候会死锁,应予以避免。

参考资料

  • https://songlh.github.io/paper/go-study.pdf

  • https://golang.org/ref/spec

  • https://golang.org/doc/effective_go.html

  • https://golang.org/pkg/

欢迎关注腾讯程序员视频号

从 bug 中学习:六大开源项目告诉你 go 并发编程的那些坑相关推荐

  1. BAT开源项目哪家强,这15个开源项目告诉你答案

    github精选前文传送门: 国产BAT开源谁最牛,这些开源项目告诉你 还担心春节抢不到票,Github标星21K,这两款开源项目值得推荐 程序员接私活必备的10款开源前端后台框架 工欲善其事,必先利 ...

  2. 学习Coding-iOS开源项目日志(二)

    继续前篇:<学习Coding-iOS开源项目日志(一)>,接着本第二篇<学习Coding-iOS开源项目日志(二)>讲解Coding-iOS开源项目. 前言:作为初级程序员,想 ...

  3. Spring Boot学习案例开源项目

    为了更好的学习并使用springboot进行开发,维护了一个springboot学习案例开源项目,涉及springboot从基础开发到企业实战,涵盖不同场景的开发案例.公司内部所有的服务都基于spri ...

  4. 【安卓学习之开源项目】 ParrotTongue:文字转语音(含LeLeTextToVoice、TextToMp3项目)

    █ [安卓学习之开源项目] ParrotTongue:文字转语音(含LeLeTextToVoice.TextToMp3项目) █ 系列文章目录 提示:这里是收集了和音频有关的文章 [安卓学习之MP3] ...

  5. 如何学习Python开源项目代码

    2019独角兽企业重金招聘Python工程师标准>>> 阅读Python开源项目代码主要有如下三个原因: 1. 在工作过程中遇到一些问题,Google和StackOverFlow等网 ...

  6. GitHub 上 15 个优秀开源项目告诉你如何上手数据科学!

    点击上方"CSDN",选择"置顶公众号" 关键时刻,第一时间送达! 作为一名狂热的数据科学爱好者,本文作者整理了 2017 年 Github 上尤为实用的数据科 ...

  7. 如何学习一个开源项目

    如何学习开源项目 google大法好 如果google大法有现成的实践,集成的技术帖子,跟着技术帖子学无疑是最好的.(注意点:要和技术贴处于同环境去学(依赖同版本,环境同环境).it行业很有意思,凡是 ...

  8. python应用于人工智能的代码_【python量化】人工智能技术在量化交易中应用的开源项目...

    这个项目收集了包括机器学习,深度学习以及强化学习在内的一些用于股票预测的模型.其中深度学习模型包括: LSTM LSTM Bidirectional LSTM 2-Path GRU GRU Bidir ...

  9. pyqt快速编程指南 源码_适合 C++ 新手学习的开源项目——在 GitHub 学编程

    作者:HelloGitHub-小鱼干 俗话说:万事开头难,学习编程也是一样.在 HelloGitHub 的群里,经常遇到有小伙伴询问编程语言如何入门方面的问题,如: 我要学习某一门编程语言,有什么开源 ...

最新文章

  1. 【Flutter】StatelessWidget 组件 ( Divider 组件 | Card 组件 | AlertDialog 组件 )
  2. 005_Spring的属性注入
  3. Redis数据类型操作(五) —— Sorted Set
  4. Apache - No space left on device / Failed to create proxy Mutex
  5. Design a high performance cache for multi-threaded environment
  6. Redis命令总结及其基础知识讲述
  7. 开源彻底改变了世界,但需要关注云和开源这 5 件事
  8. 如何用python打印田字格_如何用EXCEL做一套田字格模板?在家给小孩练习写字
  9. 次数匹配(不需要在前面加“\”)
  10. 【金万维】天联高级版客户端打开U8报错:未监听端口
  11. .NetCore控制台应用程序
  12. roaringbitmap java,BitMap与RoaringBitmap、JavaEWAH
  13. 张志华-统计机器学习
  14. Plotly Express 详细使用指南,20组案例从入门到进阶(附源代码)
  15. nginx 代理解决跨域问题
  16. 安装教程之JDK下载与安装
  17. 【bzoj4605】崂山白花蛇草水 权值线段树套KD-tree
  18. 【手把手】RPA机器人商城应用:工资单发送机器人
  19. Jeesite学习(一)
  20. Android 动态加载多版本SDK之DexClassLoader实践

热门文章

  1. varnish缓存服务器构建疑问
  2. 《用友ERP-U8(8.72版)标准财务模拟实训》——导读
  3. nodeJS下利用mongdb进行数据库操作
  4. CRM 客户关系管理
  5. 什么是 Visual VM?
  6. 中石油训练赛 - Check List(线段树维护偏序问题)
  7. CodeForces - 1327E Count The Blocks(组合数学)
  8. 中石油训练赛 - 数学问题(思维)
  9. 安卓系统手机软件_2M不到的安卓神器!有了这五款软件,iphone用户都羡慕
  10. 0007-Reverse Integer(整数反转)