作者:Dmitry Vyukov,Andrew Gerrand | Introducing the Go Race Detector

译者前言

第三篇 Go 官方博客译文,主要是关于 Go 内置的竞态条件检测工具。它可以有效地帮助我们检测并发程序的正确性。使用非常简单,只需在 go 命令加上 -race 选项即可。

本文最后介绍了两个真实场景下的竞态案例,第一个案例相对比较简单。重点在于第二个案例,这个案例比较难以理解,在原文的基础上,我也简单做了些补充,不知道是否把问题讲的足够清楚。同时,这个案例也告诉我们,任何时候我们都需要重视检测器给我们的提示,因为一不小心,你就可能为自己留下一个大坑。

概要

在程序世界中,竞态条件是一种潜伏深且很难发现的错误,如果将这样的代码部署线上,常会产生各种谜一般的结果。Go 对并发的支持让我们能非常简单就写出支持并发的代码,但它并不能阻止竞态条件的发生。

本文将会介绍一个工具帮助我们实现它。

Go 1.1 加入了一个新的工具,竞态检测器,它可用于检测 Go 程序中的竞态条件。当前,运行在 x86_64 处理器的 Linux、Mac 或 Windows 下可用。

竞态检测器的实现基于 C/C++ 的 ThreadSanitizer 运行时库,ThreadSanitier 在 Googgle 已经被用在一些内部基础库以及 Chromium上,并且帮助发现了很多有问题的代码。

ThreadSanitier 这项技术在 2012 年 9 月被集成到了 Go 上,它帮助检测出了标准库中的 42 个竞态问题。它现在已经是 Go 构建流程中的一部分,当竞态条件出现,将会被它捕获。

如何工作

竞态检测器集成在 Go 工具链,当命令行设置了 -race 标志,编译器将会通过代码记录所有的内存访问,何时以及如何被访问,运行时库也会负责监视共享变量的非同步访问。当检测到竞态行为,警告信息会把打印出来。(具体详情阅读 文章)

这样的设计导致竞态检测只能在运行时触发,这也意味着,真实环境下运行 race-enabled 的程序就变得非常重要,但 race-enabled 程序耗费的 CPU 和内存通常是正常程序的十倍,在真实环境下一直启用竞态检测是非常不切合实际的。

是否感受到了一阵凉凉的气息?

这里有几个解决方案可以尝试。比如,我们可以在 race-enabled 的情况下执行测试,负载测试和集成测试是个不错的选择,它偏向于检测代码中可能存在的并发问题。另一种方式,可以利用生产环境的负载均衡,选择一台服务部署启动竞态检测的程序。

开始使用

竞态检测器已经集成到 Go 工具链中了,只要设置 -race 标志即可启用。命令行示例如下:

$ go test -race mypkg

$ go run -race mysrc.go

$ go build -race mycmd

$ go install -race mypkg

通过具体案例体验下,安装运行一个命令,步骤如下:

$ go get -race golang.org/x/blog/support/racy

$ racy

接下来,我们介绍 2 个实际的案例。

案例 1:Timer.Reset

这是一个由竞态检测器发现的真实的 bug,这里将演示的是它的一个简化版本。我们通过 timer 实现随机间隔(0-1 秒)的消息打印,timer 会重复执行 5 秒。

首先,通过 time.AfterFunc 创建 timer,定时的间隔从 randomDuration 函数获得,定时函数打印消息,然后通过 timer 的 Reset 方法重置定时器,重复利用。

func main() {

start := time.Now()

var t *time.Timer

t = time.AfterFunc(randomDuration(), func() {

fmt.Println(time.Now().Sub(start))

t.Reset(randomDuration())

})

time.Sleep(5 * time.Second)

}

func randomDuration() time.Duration {

return time.Duration(rand.Int63n(1e9))

}

我们的代码看起来一切正常。但在多次运行后,我们会发现在某些特定情况下可能会出现如下错误:

anic: runtime error: invalid memory address or nil pointer dereference

[signal 0xb code=0x1 addr=0x8 pc=0x41e38a]

goroutine 4 [running]:

time.stopTimer(0x8, 0x12fe6b35d9472d96)

src/pkg/runtime/ztime_linux_amd64.c:35 +0x25

time.(*Timer).Reset(0x0, 0x4e5904f, 0x1)

src/pkg/time/sleep.go:81 +0x42

main.func·001()

race.go:14 +0xe3

created by time.goFunc

src/pkg/time/sleep.go:122 +0x48

什么原因?启用下竞态检测器测试下吧,你会恍然大悟的。

$ go run -race main.go

==================

WARNING: DATA RACE

Read by goroutine 5:

main.func·001()

race.go:14 +0x169

Previous write by goroutine 1:

main.main()

race.go:15 +0x174

Goroutine 5 (running) created at:

time.goFunc()

src/pkg/time/sleep.go:122 +0x56

timerproc()

src/pkg/runtime/ztime_linux_amd64.c:181 +0x189

==================

结果显示,程序中存在 2 个 goroutine 非同步读写变量 t。如果初始定时时间非常短,就可能出现在主函数还未对 t 赋值,定时函数已经执行,而此时 t 仍然是 nil,无法调用 Reset 方法。

我们只要把变量 t 的读写移到主 goroutine 执行,就可以解决问题了。如下:

func main() {

start := time.Now()

reset := make(chan bool)

var t *time.Timer

t = time.AfterFunc(randomDuration(), func() {

fmt.Println(time.Now().Sub(start))

reset

})

for time.Since(start) < 5*time.Second {

t.Reset(randomDuration())

}

}

main goroutine 完全负责 timer 的初始化和重置,重置信号通过一个 channel 负责传递。

当然,这个问题还有个更简单直接的解决方案,避免重用定时器即可。示例代码如下:

package main

import (

"fmt"

"math/rand"

"time"

)

func main() {

start := time.Now()

var f func()

f = func() {

fmt.Println(time.Now().Sub(start))

time.AfterFunc(time.Duration(rand.Int63n(1e9)), f)

}

time.AfterFunc(time.Duration(rand.Int63n(1e9)), f)

time.Sleep(5 * time.Second)

}

代码非常简洁易懂,缺点呢,就是效率相对不高。

案例 2:ioutil.Discard

这个案例的问题隐藏更深。

ioutil 包中的 Discard 实现了 io.Writer 接口,不过它会丢弃所有写入它的数据,可类比 /dev/null。可在我们需要读取数据但又不准备保存的场景下使用。它常常会和 io.Copy 结合使用,实现抽空一个 reader,如下:

io.Copy(ioutil.Discard, reader)

时间回溯至 2011 年,当时 Go 团队注意以这种方式使用 Discard 效率不高,Copy 函数每次调用都会在内部分配 32 KB 的缓存 buffer,但我们只是要丢弃读取的数据,并不需要分配额外的 buffer。我们认为,这种习惯性的用法不应该这样耗费资源。

解决方案非常简单,如果指定的 Writer 实现了 ReadFrom 方法,io.Copy(writer, reader) 调用内部将会把读取工作委托给 writer.ReadFrom(reader) 执行。

Discard 类型增加 ReadFrom 方法共享一个 buffer。到这里,我们自然会想到,这里理论上会存在竞态条件,但因为写入到 buffer 中的数据会被立刻丢弃,我们就没有太重视。

竞态检测器完成后,这段代码立刻被标记为竞态的,查看 issues/3970。这促使我们再一次思考,这段代码是否真的存在问题呢,但结论依然是这里的竞态不影响程序运行。为了避免这种 "假的警告",我们实现了 2 个版本的 black_hole buffer,竞态版本和无竞态版本。而无竞态版只会其在启用竞态检测器的时候启用。

black_hole.go,无竞态版本。

// +build race

package ioutil

// Replaces the normal fast implementation with slower but formally correct one.

func blackHole() []byte {

return make([]byte, 8192)

}

black_hole_race.go,竞态版本。

// +build !race

package ioutil

var blackHoleBuf = make([]byte, 8192)

func blackHole() []byte {

return blackHoleBuf

}

但几个月后,Brad 遇到了一个迷之 bug。经过几天调试,终于确定了原因所在,这是一个由 ioutil.Discard 导致的竞态问题。

实际代码如下:

var blackHole [4096]byte // shared buffer

func (devNull) ReadFrom(r io.Reader) (n int64, err error) {

readSize := 0

for {

readSize, err = r.Read(blackHole[:])

n += int64(readSize)

if err != nil {

if err == io.EOF {

return n, nil

}

return

}

}

}

Brad 的程序中有一个 trackDigestReader 类型,它包含了一个 io.Reader 类型字段,和 io.Reader 中信息的 hash 摘要。

type trackDigestReader struct {

r io.Reader

h hash.Hash

}

func (t trackDigestReader) Read(p []byte) (n int, err error) {

n, err = t.r.Read(p)

t.h.Write(p[:n])

return

}

举个例子,计算某个文件的 SHA-1 HASH。

tdr := trackDigestReader{r: file, h: sha1.New()}

io.Copy(writer, tdr)

fmt.Printf("File hash: %x", tdr.h.Sum(nil))

某些情况下,如果没有地方可供数据写入,但我们还是需要计算 hash,就可以用 Discard 了。

io.Copy(ioutil.Discard, tdr)

此时的 blackHole buffer 并非仅仅是一个黑洞,它同时也是 io.Reader 和 hash.Hash 之间传递数据的纽带。当多个 goroutine 并发执行文件 hash 时,它们全部共享一个 buffer,Read 和 Write 之间的数据就可能产生相应的冲突。No error 并且 No panic,但是 hash 的结果是错的。就是如此可恶。

func (t trackDigestReader) Read(p []byte) (n int, err error) {

// the buffer p is blackHole

n, err = t.r.Read(p)

// p may be corrupted by another goroutine here,

// between the Read above and the Write below

t.h.Write(p[:n])

return

}

最终,通过为每一个 io.Discard 提供唯一的 buffer,我们解决了这个 bug,排除了共享 buffer 的竞态条件。代码如下:

var blackHoleBuf = make(chan []byte, 1)

func blackHole() []byte {

select {

case b :=

return b

default:

}

return make([]byte, 8192)

}

func blackHolePut(p []byte) {

select {

case blackHoleBuf

default:

}

}

iouitl.go 中的 devNull ReadFrom 方法也做了相应修正。

func (devNull) ReadFrom(r io.Reader) (n int64, err error) {

buf := blackHole()

defer blackHolePut(buf)

readSize := 0

for {

readSize, err = r.Read(buf)

// other

}

通过 defer 将使用完的 buffer 重新发送至 blackHoleBuf,因为 channel 的 size 为 1,只能复用一个 buffer。而且通过 select 语句,我们在没有可用 buffer 的情况下,创建新的 buffer。

结论

竞态检测器,一个非常强大的工具,在并发程序的正确性检测方面有着很重要的地位。它不会发出假的提示,认真严肃地对待它的每条警示非常必要。但它并非万能,还是需要以你对并发特性的正确理解为前提,才能真正地发挥出它的价值。

试试吧!开始你的 go test -race。

竞态条件的赋值_Go 译文之竞态检测器 race相关推荐

  1. 竞态条件的赋值_《Java并发编程实战》读书笔记一:基础知识

    一.线程安全性 一个对象是否是需要是线性安全的,取决于它是否需要被多个线程访问 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要额外的同步,这个 ...

  2. 竞态条件的赋值_信号-sunshine225-51CTO博客

    一.基础知识信号产生的条件 a. 终端按键产生.如:ctrl+c(SIGINT信号),ctrl+\(SIGQUIT信号),ctrl+z(SIGTSTP信号)...... b. 系统命令和函数.如:ki ...

  3. 弯道超车老司机戏耍智能合约——竞态条件漏洞 | 漏洞解析连载之三

    安全,区块链领域举足轻重的话题,为什么一行代码能瞬间蒸发几十亿市值?合约底层函数的使用不当会引起哪些漏洞?重入漏洞会导致什么风险? 「区块链大本营」携手「链安科技」团队重磅推出「合约安全漏洞解析连载」 ...

  4. 以太坊漏洞分析————3、竞态条件漏洞

    引子:至道问学之有知无行,分温故为存心,知新为致知,而敦厚为存心,崇礼为致知,此皆百密一疏. -- 清·魏源<庸易通义> 区块链的"高速公路"在川流不息的同时,却也事故 ...

  5. 线程学习5——竞态条件

    竞态条件 概述:如果两个或两个以上的线程同时访问相同的对象,或者访问不同步的共享状态.就会出现竞态条件. 举例:如果多个线程同时访问类StateThread中的方法,最后结果会如何呢? 定义一个类St ...

  6. linux操作系统之竞态条件(时序竞态)

    (1)时序竞态:前后两次运行同一个程序,出现的结果不同. (2)pause函数:使用该函数会造成进程主动挂起,并等待信号唤醒,调用该系统调用的进程会处于阻塞状态(主动放弃CPU) 函数原型:int p ...

  7. [Linux]继续探究mysleep函数(竞态条件)

    之前我们探究过mysleep的简单用法,我们实现的代码是这样的: #include<stdio.h> #include<signal.h>void myhandler(int ...

  8. Linux系统编程----8(竞态条件,时序竞态,pause函数,如何解决时序竞态)

    竞态条件(时序竞态): pause 函数 调用该函数可以造成进程主动挂起,等待信号唤醒.调用该系统调用的进程将处于阻塞状态(主动放弃 cpu) 直 到有信号递达将其唤醒,等不到一直等 int paus ...

  9. 雪城大学信息安全讲义 五、竞态条件

    五.竞态条件 原文:Race Condition Vulnerability 译者:飞龙 1 竞态条件漏洞 下面的代码段属于某个特权程序(即 Set-UID 程序),它使用 Root 权限运行. 1: ...

  10. java 多线程 临界区_【Java并发性和多线程】竞态条件与临界区

    本文为转载学习 在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源.如,同一内存区(变量,数组,或对象).系统(数据库,web services等)或文件.实际上,这些问题只有 ...

最新文章

  1. 使用 electron-updater 自动更新应用
  2. LeetCode算法题-Reverse Linked List(Java实现)
  3. 【Flutter】底部导航栏实现 ( BottomNavigationBar 底部导航栏 | BottomNavigationBarItem 导航栏条目 | PageView )
  4. 如何在eclipse中安装git?
  5. 数据可视化组队学习:《Task05 - 样式色彩秀芳华》笔记
  6. 给定一个32位有符号整数,将整数中的数字进行翻转
  7. [Qt教程] 第37篇 网络(七)TCP(一)
  8. python flask快速入门与进阶 百度云_Python Flask快速入门与进阶
  9. java对import语句_Java的import语句 - 不积跬步,无以至千里 - 51Testing软件测试网 51Testing软件测试网-软件测试人的精神家园...
  10. PS教程第十九课:移动工具
  11. C#中的变量类型(值类型、引用类型)
  12. java线程的5个使用技巧
  13. 全球首个由AI鉴定保驾护航的B2B奢侈品潮品交易平台图灵云仓上线
  14. r语言提取列名_玩转数据处理120题之P1-P20(R语言tidyverse版本)
  15. Tensorflow2梯度带tape.Gradient的用法_(全面,深入)
  16. 从Oracle Database 角度来看浪潮天梭K1主机的操作系统选择
  17. exosip鉴权及使用
  18. 软件测评师的一些重点①
  19. overleaf表格_latex插入表格心得
  20. 大型网站之网站静态化(综合篇)

热门文章

  1. MyBatis实现一对一,一对多关联查询
  2. JAVA命令符找不到符号_java: 找不到符号 符号: 方法 setLatestEventInfoentInfo
  3. 常用会计科目名词解释
  4. 三大主流前端框架介绍VUE 、React、Angular
  5. win10运行python没有硬编码器_windows下关于python的编解码问题
  6. PLsql oracle 误删除 恢复
  7. 公众号基本的绑定手机号页面(截取code,手机号正则,验证码倒计时)
  8. Scrapy-2:东莞阳光政务平台
  9. nachos交叉编译器java_ubuntu - 编译Nachos源代码时出错“gnu / stubs-32.h:没有这样的文件或目录”...
  10. Flink中的Window计算-增量计算全量计算