go 怎么等待所有的协程完成_理解真实世界中 Go 的并发 BUG

点击上方蓝色“Go语言中文网”关注,回复「电子书」领全套Go资料

有几个学生研究归纳了go编程中的并发bugs,发表了一篇(英文)论文:《Understanding Real-World Concurrency Bugs in Go》。为你下载好了 PDF,关注公众号 Go语言中文网,回复 gostudy 获取。

在此做一个笔记,便于查阅。

文章以六个产品级go应用作为研究对象:Docker、Kubernetes、etcd、gRPC、CockroachDB、BoltDB,总共研究了这些应用中的171个bug,研究它们的根本原因,并重现这些bugs,以及检查它们的修复补丁。最后用两个现有go并发bug检测器测试了这些bug。

文章试图回答一个问题:对于两种线程/协程间通信机制,消息传递机制和共享内存机制,哪个更不容易出错?

文章从两个维度对bug进行了分类,bug原因(对共享内存的误用、对消息传递的误用)和bug表现(阻塞性bug、非阻塞性bug)。

研究结果及提交日志可以在以下地址查阅:https://github.com/system-pclub/go-concurrency-bugs

many concurrency bugs are caused by the mixed usage of message passing and other new semantics and new libraries in Go, which can easily be overlooked but hard to detect.

背景

使用共享内存实现同步

Go支持协程间共享内存,提供了多种传统的同步手段,如锁(Mutex)、读写锁(RWMutex)、条件变量(Cond)、原子读写(atomic)。go的RWMutex实现与C中的pthread_rwlock_t不同,go中的写锁请求优先级高于读锁。

go中还有一些新特性,Once保证一个函数只执行一次:使用 Once.Do(f) 方法,即使这一语句被多个协程调用了多次,也只有第一次的时候,函数f会被执行。

和C中的pthread_join类似,go使用WaitGroup来实现等待协程对其他协程的等待。

使用消息传递实现同步

channel(chan)是go的新特性,学习go语言编程的都应该熟悉了。channel分有缓冲和无缓冲两种(buffered and unbuffered)。

使用select可以从多路channel中进行选择。当有多路case有效时,select会从中随机选择一个去执行,这种随机性可能会造成bug。

Go引入了几种新机制来简化协程间的交互,如用context携带数据传递在不同协程之间,还有Pipe可在读协程和写协程之间传递流式数据。这两种都是新的消息传递机制,不注意的话可能引起新的并发bug。

Go并发模型

在研究并发bug前,文章先研究了go中的并发模型。

首先统计了那几个应用中创建gorutine的(静态)语句数量(位置数量),如下表:

51c5db798bbf7aa2bfaa04853d230633.png
img

文章觉得喜欢用匿名函数创建gorutine的多些(除了kubernetes和BoltDB),另外还发现C语言版gRPC比go语言版更少创建线程语句。

然后,文章还统计了各种同步机制的使用比例,如下图:

130a25e48fb7121cbadeb04b7d205bac.png
img

从中可以看出,共享内存机制的锁还是用得最多啊!

同时,这些机制的使用比例,随着项目时间推进,是否有什么变化趋势的?似乎没有明显变化,如下截图:

bd705a707412e60f8836145abef9fe52.png
img

Bug分类

分类如下:

b9f290f6dde90c4259d8d86a6453ad42.png
img

从数值看,阻塞性bug和非阻塞性bug出现数量差不多。

(笔者注:对于原因而言,从数值上看使用共享内存的造成bug比较多,但是这里只统计了绝对值,没有和前面共享机制的使用量结合起来考虑比例,似乎不大妥当。)

对于这些bug,文章作者使用相应有bug的版本,根据bug报告中的操作尝试重现这些bug,结果发现并发bug是很难重现的。从而这些bug存在时间都比较长,而一旦被发现,一般会比较快地得到解决。bug生存时间统计如下:

082aaeec5b2b79517d2a402c33a9a425.png
img

Bug原因分析

1、阻塞性bug

统计如下:

5960804cdb1ec3f22b299fb2b5517bc5.png
img

具体分析

(1)对共享内存保护的失误:

Mutex:28个阻塞性bug由对锁的不当使用造成,包括重复锁、以冲突的顺序申请锁、忘记解锁*。这些bug都是传统bug,文章觉得传统的死锁检测算法应该能检测出这类bug。

RWMutex:前面提到过,go中的写锁优先级高。这种实现机制可以造成如下bug:协程A对同一个RWMutex申请两次读锁,但在这两次申请中间,协程B申请写锁。此时,由于A已经持有了一个读锁,而写锁又是排他性的,所以B被阻塞。然后,A第二次申请读锁时,由于B的写锁优先级高,所以A的读锁必须排在B的写锁请求之后,导致A被阻塞。从而发生了死锁。

统计中有5个bug是由这个原因造成。由于在C语言中这种情况不会造成死锁,所以参考C语言类似机制在Go中写这样的代码,容易导致这样的bug。

Wait:3个阻塞性bug归因于等待操作无法继续。跟Mutex和RWMutex不同,这里并不涉及循环等待。有两个bug是这样的:Cond被用来保护共享内存访问,其中一个协程调用了Cond.Wait(),但是在这之后却没有别的协程调用Cond.Signal()(或Cond.Broadcast())。

另一个bug,Docker#25384,如下图所示,使用了一个共享的WaitGroup变量,造成bug主要是Wait()放在了错误的地方即第7行,修复bug只需要把Wait()挪到图中的第8行(循环外)。

71d9834f60ec4fbea259dbf0742077af.png
img

(2)对消息传递的误用

Channel:对通过channel传递消息的错误使用导致了29个阻塞性bug。很多都跟发送和接收的错配有关。如下图所示,在使用第2行代码初始化channel的情况下,在子协程执行到第6行代码前,如果超时时间到了,或者子协程执行到第6行时,select的两个case同时可用,由于select的随机性而跑到了超时的那个case,就会导致finishReq函数返回,从而子协程阻塞。这个问题的修复方法是将channel定义为缓冲channel,这样无论何种情况子协程都不会阻塞住。

32eae061f8cd4fe515d68862e02e605a.png
img

当组合使用go特定类库时,channel的创建和协程阻塞有可能被埋在了类库的调用之中。如下图所示,行1创建了一个新的context对象 hcancel,同时一个新的协程被创建,消息可以通过hcancel的channel传递到新协程。如果在行4 timeout大于0,另一个context对象在行5被创建,并且hcancel指向了新的对象。之后,将无法向协程所关联的旧对象发送消息,旧对象也没法被关闭。这个问题的避免方法是,避免创建额外的context对象。

f7dae504aec15de7f19948efc07ed5e4.png
img

Channel和其他的阻塞特性:有16个bugs,其中一个协程阻塞在Channel操作,而别的协程阻塞在锁或等待上。如下图,协程1在发送消息到ch时阻塞了,而同时协程2却被m.Lock()阻塞。解决方案是对协程1使用具有default分支的select来确保ch不再阻塞。

03124aa3ee0ca5f89cdfa91d80ecaf06.png
img

消息库函数:go提供了几种传递消息和数据的库,如Pipe。对这些的不正确使用也会造成bug。例如,和Channel类似,如果一个Pipe未关闭,Pipe的两端一个伙伴挂了,另一个伙伴等着读或写数据,那这是等着读或写数据的伙伴就被阻塞住了。类似的bug有4个。

最后,关于阻塞性bug,文章认为消息传递机制更容易造成更多类型的bug。

2、非阻塞性bug

统计如下:

ae3e8aff8fdef1ead133344f39bd49bb.png
img

(1)对共享内存的保护失败

已有很多研究发现,未保护共享内存或保护错误是造成数据竞争或其他非阻塞性bug的主要原因。本文也发现80%非阻塞性bug都归因于未保护或错误地保护共享内存。但go中的情况和传统编程语言的情况也并非完全相同。

传统bug:超过一半非阻塞性bug都是由于传统问题造成的,就跟在Java、C这些编程语言中一样,如原子操作的破坏、顺序混乱、数据竞争。有几个bug是对go新特性的不够理解造成的,如:Docker#22985 和 CockroachDB#6111 是由于将一个变量的引用通过Channel在不同协程间传递,从而造成了共享变量的竞争状态。

匿名函数:Go语言中在一个函数前加go关键字就可以启动协程,这个函数是可以没有名字的(匿名)。在匿名函数之前定义的所有局部变量,在匿名函数中都是可见的。不幸的是,由于开发者可能不够注意对这些在不同协程中的共享变量做保护,从而可能容易导致数据竞争的bug。有11个bug就是这种类型,其中9个是父协程和子协程之间的数据竞争,2个是两个子协程之间的数据竞争。如下图的一个例子,含bug的版本中,变量i在父协程和子协程之间共享了,开发者想要得到不同的i值所生成的apiVersion,但是如果在父协程的for循环结束后子协程才运行起来,那所有的apiVersion都将等于”v1.21”。解决方案就是将i作为参数传递到子协程中,此时传递的是i的拷贝。

d4c406316018e6186184a30805387ff8.png
img

WaitGroup的误用:使用WaitGroup的一个基本准则是,Add必须在Wait之前执行。有6个bug是因为违反了这条准则。如下图所示,这是etcd中的一个bug,这里是无法保证func1中行8的Add一定在func2中行5的Wait之前执行的。解决方案就是将Add操作遇到行6的位置,保证要么Add在Wait之前执行,要么根本不会执行到idle这个case。

ae3e8aff8fdef1ead133344f39bd49bb.png
img

特定库函数:go中有些类库的变量是隐式在多协程中共享的。如context就被设计为可以被多个关联协程访问。etcd#7816就是因为在多个协程中竞争使用一个context对象的一个字符串字段导致的。

另一个例子是testing包。测试函数只有一个testing.T类型的变量,这个变量用于传递测试状态如error何日志。有3个bug就是在测试函数以及测试函数内启动的子协程之间竞争使用testing.T变量导致。

(2)消息传递中的错误

channel的误用:前面也提到过,channel的使用需要遵循一定的规则,否则就会引起一些bug。如下图所示(Docker#24007),可能有多个协程会运行到这段代码,其中可能有多个跑到了select的default分支,导致对channel的多次关闭,从而引发panic。这种情况,可以使用Once.Do将关闭channel的语句包起来,保证它只会执行一次。

50d77cae4c673daae2c12f91774315b6.png
img

还有一种类型是将channel和select一起使用,当select收到多个case的消息时,是没办法保证会执行哪一个的,这种非确定性的选择,导致了3个bug。下图是一个例子,其中f函数执行耗时操作,当它执行完之后,stopCh的消息和ticker有可能同时到达,此时并不一定会执行到11行return语句,也有可能执行到case

9e842779dc599f9271ac65097b6e028a.png
img

特定库函数:一些库函数内部会使用channel,也可能导致非阻塞性bug。下图是一个与time包有关的bug。开发者想实现的是,要么收到Done信号,要么超时,然后再返回。但是含bug的版本先创建了超时时间为0的timer,然后再判断参数dur是否大于0 ,大于0的话修改timer。但是,当dur为0的情况下,timer实际上一开始就被设置为有信号了,可能导致函数过早返回。解决方案是不要让timer过早创建。

04d195d841e235d6dceb51b5a0a0d679.png
img

非阻塞性bug的检测

Go提供了数据竞争检测,在build的时候使用 -race 标志即可启用。

文章的一些结论是,消息传递机制也容易造成bug,情况并不比共享内存机制好。消息传递机制更多地会造成一些阻塞性bug,比较少造成非阻塞性bug,而且可以用于解决由于共享内存导致的非阻塞性bug。

关于bug检测,目前很多在传统语言中针对共享内存的检测算法,在go中也是适用的,但是针对go的消息传递机制所引起bug的检测,还需研究。

译者:Darlzan

译文链接:https://blog.csdn.net/notjusttech/article/details/88294964


推荐阅读

  • Socket Server的N种并发模型汇总

福利我为大家整理了一份从入门到进阶的Go学习资料礼包(下图只是部分),同时还包含学习建议:入门看什么,进阶看什么。

b1f26864550da334d85ed40e310954bc.png

关注公众号 「polarisxu」,回复 ebook获取;还可以回复「进群」,和数万 Gopher 交流学习。

fb9ac87d5d805fe6747822a178b3c36a.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/571444.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java地图图表动态亮点,可视化图表行动指南:地表最强解读来了

原标题:可视化图表行动指南:地表最强解读来了身处信息技术高速发展的时代,数据价值日益凸显,然而如何将数据更好的展示,让别人一看就懂且眼前一亮可是一门大学问。此前,小亿在广大数友的强烈要求下结合前人…

matlab cell向量匹配向量,根据2个cell格式数据中的某二列进行匹配并合并

clear allacell(9,4); % a中的第1列为样本id, 第3列为日期bcell(6,3); % b中的第1列为样本id, 第3列为日期我想根据a中的第1列对应样本id和第3列对应日期与b中的第1列对应id和第3列对应日期进行匹配合并。a{1,1}name1;a{1,2}37; a{1,3}2010/9/1; a{1,4}4.5;a{2,1}name1…

python tfidf特征变换_Spark MLlib机器学习开发指南(4)--特征提取--TF-IDF

基于最新2.2.0版本翻译本节介绍和特征一起工作的算法,大致分为以下几类:提取:从原始数据提取特征转换:缩放,转换,或者修改特征选择:从一个大的特征集合里面选择一个子集局部敏感哈希(LSH)&#…

ASP.NET MVC 使用Swagger需要注意的问题!!!

之前,一直使用的微软自带的Microsoft.AspNet.WebApi.HelpPage来作为项目的接口文档,但总感觉有些不足,就准备采用Swagger。 在项目中引用Swagger很方便,直接Nuget搜索安装Swashbuckle就可以。但是,在使用的过程中&…

小波包能量matlab,小波包分析和小波包能量介绍.doc

小波包分析和小波包能量介绍基于五次谐波和小波重构能量的配电网单相接地故障的选线方法研究*孙其东,张开如,伊利峰,宋祥民,李娅芸(山东科技大学 电气与自动化工程学院 山东 青岛 266590)摘要: 对配电网经常发生的单相接地短路故障…

分块试水--CODEVS4927 线段树练习5

模板 1 #include<stdio.h>2 #include<algorithm>3 #include<string.h>4 #include<stdlib.h>5 #include<math.h>6 //#include<bitset>7 //#include<iostream>8 using namespace std;9 10 int n,m,q;11 #define maxn 10001112 #define…

【JVM】类的生命周期【转+整理】

参考如下三篇并整理。 1.Java类加载机制详解 2.深入理解Java&#xff1a;类加载机制及反射 3.jvm系列(一):java类的加载机制 类的生命周期是从被加载到虚拟机内存中开始&#xff0c;到卸载出内存结束。过程共有七个阶段。 1.加载---2.验证---3.准备---3.解析---5.初始化---6.使…

java概念,Java基础概念

1. Java编译程序将Java源程序翻译成JVM可执行代码-Java字节码。这一过程同C/C不同。当C编译器翻译成一个对象代码时&#xff0c;该代码是为在某一特定硬件平台运行而产生的。因此&#xff0c;在编译过程中&#xff0c;编译程序通过查表将所有符合引用转换为特定的内存偏移量。而…

【Excle】如何隐藏数据透视表中的错误值

如下&#xff1a;数据透视表出现错误怎么解决呢步骤方法①单击数据透视表任意单元格→数据透视表工具→分析→选项→勾选“对于错误值显示”→确定方法②右键→数据透视表选项&#xff08;同样可以修改&#xff09;转载于:https://www.cnblogs.com/OliverQin/p/8043469.html

vue怎么让接口带上cookie_在Vue中怎么使用cookie 之 vue-cookies

cookie 在工作中比较常用, 可以自行封装一些 添加/删除/获取cookie的方法, 可参考这个在Vue中有个很好用的插件 vue-cookiesgithub地址&#xff1a;https://github.com/cmp-cc/vue-cookies我们具体来看下怎么使用这个插件的使用第一步&#xff1a;安装vue-cookienpm install vu…

php 微信分享链接怎么弄,PHP实现 微信--分享朋友链接

不是些高深知识,但是第一次做,确实费了很多的时间.终于一点一点的扣了出来. 目前已经使用中.解决这个问题,总共碰到了几次BUG,1是生成的签名不对,自己打印出来,在去微信的签名算法页面一一核对.这时候如果对了,还是不成功.那就是需要动态获取URL链接的.最后需要将 函数写在为微…

MySQL中整型和字符串类型指定长度的含义

引入&#xff1a; int(5)和char(5)或者varchar(5)中的数字指的是什么意思&#xff1f;是字节数&#xff0c;还是字符长度&#xff1f;为什么在整型中指定了int(5)却可以输入123456&#xff1f; 答案是后者&#xff0c;不管是整型还是字符串类型&#xff0c;后面跟的数字都是字符…

php网站开发项目实战,PHP动态网站开发项目实战

任务1 分析网站需求11.1 知识准备11.1.1 功能结构图11.1.2 用例图21.2 任务实现51.2.1 花公子蜂蜜网站项目功能结构分析51.2.2 花公子蜂蜜网站项目用例分析61.3 经验传递71.4 知识拓展8任务2 设计网站前台版面92.1 知识准备92.1.1 网站版面设计流程92.1.2 网站版面设计原则102.…

repo同步代码_工欲善其事,必先利其器:repo 介绍

介绍此repo非彼repo。这里的repo&#xff0c;是指谷歌公司的一款小工具&#xff0c;名字就叫repo(我承认&#xff0c;这不是一个好名字&#xff0c;Google里面怎么搜都搜不到它的真身)。解决的问题有时候&#xff0c;我们需要在一台电脑上克隆很多个代码仓库&#xff0c;编译它…

洛谷 P1754 球迷购票问题

P1754 球迷购票问题 题目背景 盛况空前的足球赛即将举行。球赛门票售票处排起了球迷购票长龙。 按售票处规定&#xff0c;每位购票者限购一张门票&#xff0c;且每张票售价为50元。在排成长龙的球迷中有N个人手持面值50元的钱币&#xff0c;另有N个人手持面值100元的钱币。假设…

php date函数实现,PHP date() 函数可实现的功能列表

date() 函数格式化本地日期和时间&#xff0c;并返回已格式化的日期字符串。date(format,timestamp);format 参数为必填&#xff0c;格式为何种格式timestamp 可选参数。规定整数的 Unix 时间戳。默认是当前的本地时间(time())。format参数如下&#xff1a;d - 一个月中的第几天…

ddd 企业应用架构模式_灵魂拷问:用了DDD分包就是落地了领域驱动设计吗?谈谈DDD本质...

学习DDD的时候&#xff0c;作为开发&#xff0c;我们更关心它在技术层面的东西&#xff0c;尤其体现在DDD的分包方式、编码技巧等方面。自然的&#xff0c;我们不禁发问&#xff0c;用了DDD的分包&#xff0c;就是实践落地了DDD了么&#xff1f;不卖关子&#xff0c;直接说答案…

java常见编码

摘自&#xff1a;http://www.cnblogs.com/yaya-yaya/p/5768616.html红色 主要点  灰色 内容 绿色 知识点 橘色 补充内容几种常见的编码格式 为什么要编码 不知道大家有没有想过一个问题&#xff0c;那就是为什么要编码&#xff1f;我们能不能不编码&#xff1f;要…

准确率 召回率_机器学习中F值(F-Measure)、准确率(Precision)、召回率(Recall)

在机器学习、数据挖掘、推荐系统完成建模之后&#xff0c;需要对模型的效果做评价。业内目前常常采用的评价指标有准确率(Precision)、召回率(Recall)、F值(F-Measure)等&#xff0c;下图是不同机器学习算法的评价指标。下文讲对其中某些指标做简要介绍。本文针对二元分类器&am…

php 前置操作方法,前置操作-THINKPHP 5.0 手册最新版

前置操作可以为某个或者某些操作指定前置执行的操作方法&#xff0c;设置 beforeActionList属性可以指定某个方法为其他方法的前置操作&#xff0c;数组键名为需要调用的前置方法名&#xff0c;无值的话为当前控制器下所有方法的前置方法。[except > 方法名,方法名]表示这些…