当我意识到我一直在处理和解决的问题有一个专有名词描述的时候,我总会觉得这事十分有趣。这次出现这种情况的是竞争条件(Race Conditions)。当你处理多个 routine 共享某类资源的时候,不可避免的需要考虑到这个。如果你未曾在你的代码中考虑过这个问题,现在就是一个不错的时候。

竞争条件是:当两个或两个以上的 routines 访问相同资源的时候,例如一个变量或者结构体,并且在不考虑其他 Gorutines 的情况下对资源进行读/写。这类代码能够造成你所能见到的最令人头疼,最随机的 bugs。通常需要大量的日志和一些运气才能找到这些类型的错误。这么多年来,我完善了我的日志的技能,以便识别这些问题。

Go 工具套件在 Go 版本 1.1 引入了一个竞态检测工具(race detector)。这个竞态检测工具是在编译流程中内置到你程序的代码。一旦你的程序开始运行,它能够发现和报告任何他所检测到的竞态情况。 这是非常棒的功能, 为识别缺陷代码做出了非常重要的工作。

让我们写一个非常的简单的包含竞态条件内置竞态检测代码的程序。

package mainimport ("fmt""sync"
)var Wait sync.WaitGroup
var Counter int = 0func main() {for routine := 1; routine <= 2; routine++ {Wait.Add(1)go Routine(routine)}Wait.Wait()fmt.Printf("Final Counter: %dn", Counter)
}func Routine(id int) {for count := 0; count < 2; count++ {value := Countervalue++Counter = value}Wait.Done()
}

这个程序看起来没有问题。它创建了两个协程,每一个协程都会增加全局变量 Counter 两次。当他们都运行结束后,程序显示全局变量 Counter 的值。当我运行这个程序的时候,他会显示正确答案 4。所以这个程序工作正常,但真的吗?

让我们通过 Go 竞态检测运行这个代码,看看它会发现什么? 在代码所在的目录打开终端,以 -race 参数编译代码。

go build -race

然后程序输出

==================
WARNING: DATA RACE
Read by goroutine 5:main.Routine()/Users/bill/Spaces/Test/src/test/main.go:29 +0x44gosched0()/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9fPrevious write by goroutine 4:main.Routine()/Users/bill/Spaces/Test/src/test/main.go:33 +0x65gosched0()/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9fGoroutine 5 (running) created at:main.main()/Users/bill/Spaces/Test/src/test/main.go:17 +0x66runtime.main()/usr/local/go/src/pkg/runtime/proc.c:182 +0x91Goroutine 4 (finished) created at:main.main()/Users/bill/Spaces/Test/src/test/main.go:17 +0x66runtime.main()/usr/local/go/src/pkg/runtime/proc.c:182 +0x91==================
Final Counter: 4
Found 1 data race(s)

看起来,工具在代码中检测到竞争条件。如果你查看上面的竞争条件报告,你会看到针对程序的输出。全局变量 Counter 的值是 4。这就是这类的 bug 的难点所在,代码大部分情况是工作正常的,但错误的情况会随机产生。竞争检测告诉我们隐藏在代码中的糟糕问题。

警告报告告诉我们问题发生的准确位置:

Read by goroutine 5:main.Routine()/Users/bill/Spaces/Test/src/test/main.go:29 +0x44gosched0()/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9fvalue := CounterPrevious write by goroutine 4:main.Routine()/Users/bill/Spaces/Test/src/test/main.go:33 +0x65gosched0()/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9fCounter = valueGoroutine 5 (running) created at:main.main()/Users/bill/Spaces/Test/src/test/main.go:17 +0x66runtime.main()/usr/local/go/src/pkg/runtime/proc.c:182 +0x91go Routine(routine)

你能发现竞争检测器指出两行读和写全局变量 Counter 的代码。同时也指出生成协程的代码。

让我们对代码进行简单修改,让竞争情况更容易暴露出来。

package mainimport ("fmt""sync""time"
)var Wait sync.WaitGroup
var Counter int = 0func main() {for routine := 1; routine <= 2; routine++ {Wait.Add(1)go Routine(routine)}Wait.Wait()fmt.Printf("Final Counter: %dn", Counter)
}func Routine(id int) {for count := 0; count < 2; count++ {value := Countertime.Sleep(1 * time.Nanosecond)value++Counter = value}Wait.Done()
}

我在循环中增加了一个纳秒的暂停。这个暂停正好位于协程读取全局变量 Couter 存储到本地副本之后。让我们运行这个程序看看在这种修改之后,全局变量 Counter 的值是什么?

Final Counter: 2

循环中的暂停导致程序的失败。Counter 变量的值不再是 4 而是 2。发生了什么? 让我们深挖代码看看为什么这个纳秒的暂停会导致这个 Bug。

在没有暂停的情况下,代码运行如下图:

没有暂停的情况下,第一个协程被生成,并且完成执行,紧接着第二个协程才开始运行。这就是为什么程序看起来像正确运行的原因,因为它在我的电脑上运行速度非常快,以至于代码自行排队运行。

让我们看看在有暂停的情况下,代码如何运行:

上图已经展示了所有必要的信息,因此我就没有把他全部画出来。这个暂停导致运行的两个协程之间进行了一次上下文切换。这次我们有一个完全不同的情况。让我们看看图中展示的代码:

value := Countertime.Sleep(1 * time.Nanosecond)value++Counter = value

在每一次循环的迭代过程中,全局变量 Counter 的值都被暂存到本地变量 value,本地的副本自增后,最终写回全局变量 Counter。如果这三行代码在没有中断的情况下,没有立即运行,那么程序就会出现问题。上面的图片展示了全局变量 Counter 的读取和上下文切换是如何导致问题的。

在这幅图中,在被协程 1 增加的变量被写回全局变量 Counter 之前,协程 2 被唤醒并读取全局变量 Counter。实质上,这两个协程对全局Counter变量执行完全相同的读写操作,因此最终的结果才是 2。

为了解决这个问题,你也许认为我们只需要将增加全局变量 Counter 的三行代码改写减少到一行即可。

package mainimport ("fmt""sync""time"
)var Wait sync.WaitGroup
var Counter int = 0func main() {for routine := 1; routine <= 2; routine++ {Wait.Add(1)go Routine(routine)}Wait.Wait()fmt.Printf("Final Counter: %dn", Counter)
}func Routine(id int) {for count := 0; count < 2; count++ {Counter = Counter + 1time.Sleep(1 * time.Nanosecond)}Wait.Done()
}

当我们运行这个版本的代码的时候,我们会再次得到正确的结果:

Final Counter: 4

如果我们启动竞争检测来运行该代码,上面出现的问题应该会消失:

go build -race

并且输出为:

==================
WARNING: DATA RACE
Write by goroutine 5:main.Routine()/Users/bill/Spaces/Test/src/test/main.go:30 +0x44gosched0()/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9fPrevious write by goroutine 4:main.Routine()/Users/bill/Spaces/Test/src/test/main.go:30 +0x44gosched0()/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9fGoroutine 5 (running) created at:main.main()/Users/bill/Spaces/Test/src/test/main.go:18 +0x66runtime.main()/usr/local/go/src/pkg/runtime/proc.c:182 +0x91Goroutine 4 (running) created at:main.main()/Users/bill/Spaces/Test/src/test/main.go:18 +0x66runtime.main()/usr/local/go/src/pkg/runtime/proc.c:182 +0x91==================
Final Counter: 4
Found 1 data race(s)

然而,在这三十行代码的程序中,我们仍然检测到一个竞争条件。

Write by goroutine 5:main.Routine()/Users/bill/Spaces/Test/src/test/main.go:30 +0x44gosched0()/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9fCounter = Counter + 1Previous write by goroutine 4:main.Routine()/Users/bill/Spaces/Test/src/test/main.go:30 +0x44gosched0()/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9fCounter = Counter + 1Goroutine 5 (running) created at:main.main()/Users/bill/Spaces/Test/src/test/main.go:18 +0x66runtime.main()/usr/local/go/src/pkg/runtime/proc.c:182 +0x91go Routine(routine)

使用一行代码进行增加操作的程序正确地运行了。但为什么代码仍然有一个竞态条件? 不要被我们用于递增 Counter 变量的一行Go代码所欺骗。让我们看看这一行代码生成的汇编代码:

0064 (./main.go:30) MOVQ Counter+0(SB),BX ; Copy the value of Counter to BX
0065 (./main.go:30) INCQ ,BX              ; Increment the value of BX
0066 (./main.go:30) MOVQ BX,Counter+0(SB) ; Move the new value to Counter

实际上是执行这三行汇编代码增加 counter 变量。他们十分诡异地看起来像最初的 Go 代码。上下文切换可能发生在这三行汇编的中的任意一行后面。尽管这个程序正常工作了,但严格来说,Bug 仍然存在。

尽管我使用的例子非常简单,它还是体现发现这种 Bug 的复杂性。任何一行由 Go 编译器产生的汇编代码都有可能因为下文切换而停止运行。我们的 Go 代码也许看起来能够安全地访问资源,实际上底层汇编代码可能漏洞百出。

为了解决这类问题,我们需要确保读写全局变量 Counter 总是在任何其他协程访问该变量之前完成。管道(channle)能够帮助我们有序地访问资源。这一次,我会使用一个互斥锁(Mutex):

package mainimport ("fmt""sync""time"
)var Wait sync.WaitGroup
var Counter int = 0
var Lock sync.Mutexfunc main() {for routine := 1; routine <= 2; routine++ {Wait.Add(1)go Routine(routine)}Wait.Wait()fmt.Printf("Final Counter: %dn", Counter)
}func Routine(id int) {for count := 0; count < 2; count++ {Lock.Lock()value := Countertime.Sleep(1 * time.Nanosecond)value++Counter = valueLock.Unlock()}Wait.Done()
}

以竞态检测的模式,编译程序,查看运行结果:

go build -race
./testFinal Counter: 4

这一次,我们得到了正确的结果,并且没有发现任何竞态条件。这个程序是没有问题的。互斥锁保护了在 Lock 和 Unlock 之间的代码,确保了一次只有一个协程执行该段代码。

你可以通过以下文章学习更多例子,更好地理解 Go 竞态检测器:

http://blog.golang.org/race-detector

如果你使用了多个协程,那么使用竞态检测器测试你的代码是个不错的建议。它会在单元测试和质量保证测试中,为你节省大量的时间和麻烦。Go 开发人员能有这样的工具是很幸运地,所以值得学习一下。


eclipse运行go test_在 Go 中发现竞态条件 (Race Conditions)相关推荐

  1. 什么是竞态条件?你怎样发现和解决竞争?

    当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件. 在临界区中使用适当的同步就可以避免竞态条件. 界区实现方法有两种,一种是用 synchronized ,一种是用 Lock 显式 ...

  2. 并发编程中的锁、条件变量和信号量

    在并发编程中,经常会涉及到锁.条件变量和信号量.本文从并发开始,探究为什么需要它们,它们的概念,实现原理以及应用. 并发简介 并发是指多个事情,在同一时间段内同时发生了.和并发经常一起被提到的是并行. ...

  3. Eclipse运行jsp文件,发现页面无法访问!

    Eclipse运行jsp文件,发现页面无法访问! 解决方法是:点击Windows(窗口)选项,选择Web Browser,我选的是第一个0 internal Web Browser,说明eclipse ...

  4. 运行tomcat时,解析 HTTP 请求 header 错误 Note: further occurrences of HTTP 在方法名称中发现无效的字符串, HTTP 方法名必须是有效的符号

    启动成功后报错 org.apache.coyote.http11.Http11Processor.service 解析 HTTP 请求 header 错误 Note: further occurren ...

  5. Eclipse运行Applet没有显示图片,getCodeBase,getDocumentBase

    解决Eclipse运行Applet没有显示图片      创建一个简单的Java Applet项目用于显示图片,代码如下:[下载]:http://download.csdn.net/detail/gu ...

  6. Eclipse运行tomcat出现错误“An incompatible version [1.1.33] of the APR based。。。 ”问题的解决

    转载请注明出处:http://blog.csdn.net/dongdong9223/article/details/80009634 本文出自[我是干勾鱼的博客] 使用Eclipse运行tomcat时 ...

  7. 第一阶 01 初识Java 安装Eclipse运行及推荐软件

    学习目标 * 能够说出JDK,JRE,JVM各自的作用和关系 * 能够完成HelloWorld案例的编写及运行 * 能够使用注释对程序进行说明 * 能够知道标识符.关键字的特点 * 能够知道常量的分类 ...

  8. Eclipse运行jsp文件(基于tomcat),界面404或者显示无法访问

    小记一下,运行web项目时遇到的问题. Tmocat在cmd中可以通过startup.bat正常启动,然后在浏览器可以访问http://localhost:8080界面,可通过shutdown.bat ...

  9. eclipse 运行jsp文件

    使用eclipse运行jsp文件时,再三确认配置的Tomcat服务器与建立的动态网页无误后,点击运行总是弹出如下错误: 经过确认端口未被占用,但是仍然提示这种错误,后来发现是自己通过Tomcat安装目 ...

最新文章

  1. 在3ds Max和Vray中创建赛博朋克圣诞老人室内场景
  2. 怎么从Android App Bundle (.aab)提取和转换apks文件(从AAB到APKs的转换和提取)
  3. 汇编语言ac和c何时为1,汇编语言课堂练习一(参考答案)
  4. IIS7中的站点、应用程序和虚拟目录详细介绍
  5. 亮度 调整_摄影后期必备!如何用亮度蒙版技术精准调整照片影调
  6. qtcreator 用cmake找不到userPath的解决方法
  7. 四、RabbitMQ消息消费时的权衡
  8. ionic3编译到Android 相关配置
  9. Nginx 转发配置
  10. CVR预估建模中样本的问题
  11. flink如何设置以每天零点到第二天零点为区间的window进行计算
  12. 音乐网站制作之音乐播放
  13. NISP管理中心|NISP二级证书介绍
  14. nginx实现静态文件的token认证
  15. 电脑截图快捷键有哪些?5大截图方法总结!(2023版)
  16. freertos使用基础
  17. 20211129编译RK3399的Android发生编译服务器的CPU看门狗软件死锁的问题
  18. 得到 K 个黑块的最少涂色次数
  19. 关于GPL协议的理解(开源与商用、免费与收费的理解)
  20. IDEA解决打开properties乱码问题

热门文章

  1. 用Python3.6操作HBase之HBase-Thrift
  2. 将图片转化为txt文本显示
  3. quartus仿真19:JK触发器序列1001检测器
  4. 贺利坚老师汇编课程54笔记:标志寄存器
  5. c语言鸢尾花智能分类,基于LogisticRegression的鸢尾花分类
  6. volatile 线程内存模型
  7. 葡萄城报表介绍:数据钻取
  8. java第一次作业0
  9. [转载] Python基本语法之:字符串和字典介绍
  10. [转载] Python正则表达式(含正则表达式速查表)