内容简介:当我意识到我一直在处理和解决的问题有一个专有名词描述的时候,我总会觉得这事十分有趣。这次出现这种情况的是竞争条件(Race Conditions)。当你处理多个 routine 共享某类资源的时候,不可避免的需要考虑到这个。如果你未曾在你的代码中考虑过这个问题,现在就是一个不错的时候。竞争条件是:当两个或两个以上的 routines 访问相同资源的时候,例如一个变量或者结构体,并且在不考虑其他 Gorutines 的情况下对资源进行读/写。这类代码能够造成你所能见到的最令人头疼,最随机的 bugs。通常需
当我意识到我一直在处理和解决的问题有一个专有名词描述的时候,我总会觉得这事十分有趣。这次出现这种情况的是竞争条件(Race Conditions)。当你处理多个 routine 共享某类资源的时候,不可避免的需要考虑到这个。如果你未曾在你的代码中考虑过这个问题,现在就是一个不错的时候。
竞争条件是:当两个或两个以上的 routines 访问相同资源的时候,例如一个变量或者结构体,并且在不考虑其他 Gorutines 的情况下对资源进行读/写。这类代码能够造成你所能见到的最令人头疼,最随机的 bugs。通常需要大量的日志和一些运气才能找到这些类型的错误。这么多年来,我完善了我的日志的技能,以便识别这些问题。
Go 工具套件在 Go 版本 1.1 引入了一个竞态检测工具(race detector)。这个竞态检测 工具 是在编译流程中内置到你程序的代码。一旦你的程序开始运行,它能够发现和报告任何他所检测到的竞态情况。 这是非常棒的功能, 为识别缺陷代码做出了非常重要的工作。
让我们写一个非常的简单的包含竞态条件内置竞态检测代码的程序。
package main
import (
"fmt"
"sync"
)
var Wait sync.WaitGroup
var Counter int = 0
func main() {
for routine := 1; routine <= 2; routine++ {
Wait.Add(1)
go Routine(routine)
}
Wait.Wait()
fmt.Printf("Final Counter: %d\n", Counter)
}
func Routine(id int) {
for count := 0; count < 2; count++ {
value := Counter
value++
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 +0x44
gosched0()
/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9f
Previous write by goroutine 4:
main.Routine()
/Users/bill/Spaces/Test/src/test/main.go:33 +0x65
gosched0()
/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9f
Goroutine 5 (running) created at:
main.main()
/Users/bill/Spaces/Test/src/test/main.go:17 +0x66
runtime.main()
/usr/local/go/src/pkg/runtime/proc.c:182 +0x91
Goroutine 4 (finished) created at:
main.main()
/Users/bill/Spaces/Test/src/test/main.go:17 +0x66
runtime.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 +0x44
gosched0()
/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9f
value := Counter
Previous write by goroutine 4:
main.Routine()
/Users/bill/Spaces/Test/src/test/main.go:33 +0x65
gosched0()
/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9f
Counter = value
Goroutine 5 (running) created at:
main.main()
/Users/bill/Spaces/Test/src/test/main.go:17 +0x66
runtime.main()
/usr/local/go/src/pkg/runtime/proc.c:182 +0x91
go Routine(routine)
你能发现竞争检测器指出两行读和写全局变量 Counter 的代码。同时也指出生成协程的代码。
让我们对代码进行简单修改,让竞争情况更容易暴露出来。
package main
import (
"fmt"
"sync"
"time"
)
var Wait sync.WaitGroup
var Counter int = 0
func main() {
for routine := 1; routine <= 2; routine++ {
Wait.Add(1)
go Routine(routine)
}
Wait.Wait()
fmt.Printf("Final Counter: %d\n", Counter)
}
func Routine(id int) {
for count := 0; count < 2; count++ {
value := Counter
time.Sleep(1 * time.Nanosecond)
value++
Counter = value
}
Wait.Done()
}
我在循环中增加了一个纳秒的暂停。这个暂停正好位于协程读取全局变量 Couter 存储到本地副本之后。让我们运行这个程序看看在这种修改之后,全局变量 Counter 的值是什么?
Final Counter: 2
循环中的暂停导致程序的失败。Counter 变量的值不再是 4 而是 2。发生了什么? 让我们深挖代码看看为什么这个纳秒的暂停会导致这个 Bug。
在没有暂停的情况下,代码运行如下图:
没有暂停的情况下,第一个协程被生成,并且完成执行,紧接着第二个协程才开始运行。这就是为什么程序看起来像正确运行的原因,因为它在我的电脑上运行速度非常快,以至于代码自行排队运行。
让我们看看在有暂停的情况下,代码如何运行:
上图已经展示了所有必要的信息,因此我就没有把他全部画出来。这个暂停导致运行的两个协程之间进行了一次上下文切换。这次我们有一个完全不同的情况。让我们看看图中展示的代码:
value := Counter time.Sleep(1 * time.Nanosecond) value++ Counter = value
在每一次循环的迭代过程中,全局变量 Counter 的值都被暂存到本地变量 value,本地的副本自增后,最终写回全局变量 Counter。如果这三行代码在没有中断的情况下,没有立即运行,那么程序就会出现问题。上面的图片展示了全局变量 Counter 的读取和上下文切换是如何导致问题的。
在这幅图中,在被协程 1 增加的变量被写回全局变量 Counter 之前,协程 2 被唤醒并读取全局变量 Counter。实质上,这两个协程对全局Counter变量执行完全相同的读写操作,因此最终的结果才是 2。
为了解决这个问题,你也许认为我们只需要将增加全局变量 Counter 的三行代码改写减少到一行即可。
package main
import (
"fmt"
"sync"
"time"
)
var Wait sync.WaitGroup
var Counter int = 0
func main() {
for routine := 1; routine <= 2; routine++ {
Wait.Add(1)
go Routine(routine)
}
Wait.Wait()
fmt.Printf("Final Counter: %d\n", Counter)
}
func Routine(id int) {
for count := 0; count < 2; count++ {
Counter = Counter + 1
time.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 +0x44
gosched0()
/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9f
Previous write by goroutine 4:
main.Routine()
/Users/bill/Spaces/Test/src/test/main.go:30 +0x44
gosched0()
/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9f
Goroutine 5 (running) created at:
main.main()
/Users/bill/Spaces/Test/src/test/main.go:18 +0x66
runtime.main()
/usr/local/go/src/pkg/runtime/proc.c:182 +0x91
Goroutine 4 (running) created at:
main.main()
/Users/bill/Spaces/Test/src/test/main.go:18 +0x66
runtime.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 +0x44
gosched0()
/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9f
Counter = Counter + 1
Previous write by goroutine 4:
main.Routine()
/Users/bill/Spaces/Test/src/test/main.go:30 +0x44
gosched0()
/usr/local/go/src/pkg/runtime/proc.c:1218 +0x9f
Counter = Counter + 1
Goroutine 5 (running) created at:
main.main()
/Users/bill/Spaces/Test/src/test/main.go:18 +0x66
runtime.main()
/usr/local/go/src/pkg/runtime/proc.c:182 +0x91
go 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 main
import (
"fmt"
"sync"
"time"
)
var Wait sync.WaitGroup
var Counter int = 0
var Lock sync.Mutex
func main() {
for routine := 1; routine <= 2; routine++ {
Wait.Add(1)
go Routine(routine)
}
Wait.Wait()
fmt.Printf("Final Counter: %d\n", Counter)
}
func Routine(id int) {
for count := 0; count < 2; count++ {
Lock.Lock()
value := Counter
time.Sleep(1 * time.Nanosecond)
value++
Counter = value
Lock.Unlock()
}
Wait.Done()
}
以竞态检测的模式,编译程序,查看运行结果:
go build -race ./test Final Counter: 4
这一次,我们得到了正确的结果,并且没有发现任何竞态条件。这个程序是没有问题的。互斥锁保护了在 Lock 和 Unlock 之间的代码,确保了一次只有一个协程执行该段代码。
你可以通过以下文章学习更多例子,更好地理解 Go 竞态检测器:
http://blog.golang.org/race-detector
如果你使用了多个协程,那么使用竞态检测器测试你的代码是个不错的建议。它会在单元测试和质量保证测试中,为你节省大量的时间和麻烦。Go 开发人员能有这样的工具是很幸运地,所以值得学习一下。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Beginning Google Maps API 3
Gabriel Svennerberg / Apress / 2010-07-27 / $39.99
This book is about the next generation of the Google Maps API. It will provide the reader with the skills and knowledge necessary to incorporate Google Maps v3 on web pages in both desktop and mobile ......一起来看看 《Beginning Google Maps API 3》 这本书的介绍吧!