Golnag在现实世界的并发错误【翻译】

了解Golang在现实中的并发错误

本文地址
原文地址

编程(或数据)模型的设计不仅使得某些问题更容易(或更难)解决,而且使得某些类别的bug更容易(或更难)创建、检测和随后修复。今天的论文选择研究围棋中的并发机制。在我们深入之前,停下来考虑一下你自己对围棋的看法可能会很有趣,其中很可能包括以下几点:

  • Go的明确设计是为了使并发编程更容易,更不容易出错
  • Go使并发编程变得更容易,更不容易出错
  • Go程序大量使用通过通道传递的消息,这比共享内存同步更不容易出错
  • Go程序具有较少的并发错误
  • Go的内置死锁和数据竞争检测器将捕获任何(大多数?)错误确实会溜进代码中

第一种说法是正确的。对于其余的陈述,你可以使用这项研究的数据来重新评估你对这些观点的坚持程度…

我们对实时程序中的并发错误进行了首次系统研究。我们研究了六个流行的围棋软件[项目],包括Docker、Kubernetes和gRPC。我们总共分析了171个并发错误,其中一半以上是由非传统的Go特定问题引起的。除了这些bug的根本原因之外,我们还研究了它们的修复,进行了复制它们的实验,并用两个公开可用的Go bug检测器对它们进行了评估。

所研究的六个应用程序是Docker、Kubernetes等、CockroachDB、gRPC和BoltDB,所以这里有很多重要的现实世界围棋代码。

Table 1

分析首先研究这些应用程序实际上是如何利用Go并发原语的,然后从它们的问题跟踪器中研究并发相关的错误。这些bug分为两个主要方面:观察到的行为(阻塞或非阻塞),以及导致这种情况的并发原语的类型(共享内存或消息传递)。让我们从Go中主要并发机制的快速回顾开始。

Go的并发特性

Go的一个主要设计目标是改进传统的多线程编程语言,使并发编程更容易,更不容易出错。为此,Go将其多线程设计集中在两个原则上:1)使线程(称为goroutines)轻量级且易于创建;2)使用显式消息传递(通过通道)跨线程通信。

Goroutines是轻量级用户级线程(“绿色”线程)。goroutine是通过在函数调用之前添加关键字go来创建的,包括添加到匿名函数中。匿名函数之前声明的局部变量可以在其中访问,并且可能是共享的。通道用于在goroutines之间发送数据和状态,可以是缓冲的或无缓冲的。当使用无缓冲信道时,一个goroutine将在发送(接收)时阻塞,直到另一个goroutine正在接收(发送)。select语句允许一个goroutine等待多个通道操作,如果一个以上的情况有效,Go随机选择一个。Go也有传统的同步原语,包括互斥体、条件变量和原子变量。

Go并发的最佳实践

这六个应用程序相对大量地使用goroutines,尤其是匿名函数。

Table 2

一个特别有趣的比较可以在gRPC的例子中进行,它有一个C实现和一个Go实现。下表显示了在处理相同数量的请求时,在gRPC-Go中创建的路由数与gRPC-C中创建的路由数的比率。

Table 3

在这个比较中,goroutines的生命周期比在C版本中创建的线程要短,但是创建的频率要高得多。这种更频繁地使用戈鲁丁语是围棋语言鼓励的。

如果我们全面考察所有应用程序中并发原语的使用,一个更令人惊讶的发现是共享内存同步操作仍然比消息传递使用得更频繁:

Table 4

最常用消息传递原语是change,占所有用法的18.5%到43%。因此,我们有这样一种情况,即传统的共享内存通信仍然被大量使用,同时还有大量的消息传递原语。从bug的角度来看,这意味着我们拥有共享内存通信提供的所有令人兴奋的bug可能性,以及消息传递提供的所有bug可能性,而引起的bug是这两种风格的交互!

Go并发BUG

作者搜索了GitHub应用程序的提交历史,以找到修复并发错误的提交(3,211)。从这171个样本中随机选择进行研究。

bug分为阻塞bug和非阻塞bug。当一个或多个goroutines在执行过程中无意中被卡住并且无法前进时,就会出现阻塞错误。这个定义是更广泛的死锁,可以包括没有循环等待的情况,而是等待其他goroutines没有提供的资源。85个bug是阻塞的,86个是非阻塞的。

bug还根据它们是与共享内存保护(105个bug)还是与消息传递(66个bug)相关,在第二维度进行分类。

Table 5

阻塞方面的BUG

首先看看阻塞错误,其中42%与共享内存有关,58%与消息传递有关。回想一下,共享内存原语比消息传递原语使用得更广泛。

与消息传递不太容易出错的普遍看法相反,在我们研究的Go应用程序中,错误的消息传递比错误的共享内存保护引起的阻塞错误更多。

基于共享内存的bug包括所有常见的疑点,由于Go实现了RWMutex和Wait(见5.1.1),出现了一些新的转折。

对于与消息传递相关的错误,其中许多是由于缺少通道上的发送或接收,或者关闭通道。

所有由消息传递引起的阻塞错误都与Go的新消息传递语义(如通道)相关。它们可能很难检测,尤其是当消息传递操作与其他同步原语一起使用时。与普遍看法相反,消息传递比共享内存会导致更多的阻塞错误。

调查这些bug的修复表明,一旦理解,修复就相当简单,修复的类型与bug原因相关联。这表明,在围棋中使用全自动或半自动工具来修复阻塞错误可能是一个有希望的方向。

Table 7

Go的内置死锁检测器只能检测到研究中重现的21个阻塞错误中的两个。

非阻塞方面BUG

与消息传递相比,滥用共享内存原语导致的非阻塞错误更多。其中大约一半是由“传统的”共享内存错误模式造成的。还有几个错误是由于对Go语言特性缺乏理解造成的,特别是在goroutine中使用的匿名函数之前声明的局部变量的共享,以及WaitGroups的语义。

Table 9

为简化多线程编程而引入的新编程模型和新库本身可能会导致更多并发错误。

虽然基于消息传递的非阻塞bug相对不太常见,“在一种语言中,消息传递的复杂设计会导致这些bug在与其他特定语言特性结合时尤其难以找到。

有趣的是,程序员修复由共享内存原语误用引起的错误时,更喜欢使用消息传递来完成。

Go的数据竞争检测器能够检测到研究中一半重现的非阻塞错误。

总结

令人惊讶的是,我们的研究表明,通过消息传递和共享内存一样容易产生并发错误,有时甚至更容易。

程序员必须清楚地理解:

  • 使用匿名函数创建goroutine
  • 缓冲通道与非缓冲通道的使用
  • 使用select等待多通道操作的不确定性
  • 正确使用特殊依赖时间

尽管这些特性都是为了简化多线程编程而设计的,但实际上,很难用它们编写正确的Go程序。

很遗憾,我没有足够的空间来包含许多突出bug细节的说明性代码示例。如果你在围棋中积极发展,整篇论文很值得一读来研究它们。


 Previous
【转】深度解密Go语言之 map 【转】深度解密Go语言之 map
这是你自定义的文章摘要内容,如果这个属性有值,文章卡片摘要就显示这段文字,否则程序会自动截取文章的部分内容作为摘要
2019-05-22
Next 
基于Gin的Metrics中间件设计 基于Gin的Metrics中间件设计
这是你自定义的文章摘要内容,如果这个属性有值,文章卡片摘要就显示这段文字,否则程序会自动截取文章的部分内容作为摘要
2019-05-09
  TOC