Go
语言以容易进行并发编程而闻名,可是若是稍不注意,并发程序可能致使的数据竞争问题(data race)就会常常出如今你编写的并发程序的待解决Bug
列表中-- 若是你不幸在代码中遇到这种错误,这将是最难调试的错误之一。编程
今天这篇文章里咱们首先来看一个致使数据竞争的示例程序,使用go
命令行工具检测程序的竞争状况。而后咱们将介绍一些在不改变程序核心逻辑的状况下如何绕过并解决并发状况下的数据竞争问题的方法。最后咱们会分析用什么方法解决数据竞争更合理以及留给你们的一个思考题。并发
本周这篇文章的主旨概要以下:函数
go
命令行工具检测程序的竞争状况。要解释什么是数据竞争咱们先来看一段程序:工具
package main import "fmt" func main() { fmt.Println(getNumber()) } func getNumber() int { var i int go func() { i = 5 }() return i }
上面这段程序getNumber
函数中开启了一个单独的goroutine
设置变量i
的值,同时在不知道开启的goroutine
是否已经执行完成的状况下返回了i
。因此如今正在发生两个操做:测试
i
的值正在被设置成5。getNumber
返回了变量i
的值。如今,根据这两个操做中哪个先完成,最后程序打印出来的值将是0或5。ui
这就是为何它被称为数据竞争:getNumber
返回的值根据操做1或操做2中的哪个最早完成而不一样。spa
下面的两张图描述了返回值的两种可能的状况对应的时间线:命令行
你能够想象一下,每次调用代码时,代码表现出来的行为都不同有多可怕。这就是为何数据竞争会带来如此巨大的问题。调试
咱们上面代码是一个高度简化的数据竞争示例。在较大的应用程序中,仅靠本身检查代码很难检测到数据竞争。幸运的是,Go
(从V1.1开始)有一个内置的数据竞争检测器,咱们可使用它来肯定应用程序里潜在的数据竞争条件。code
使用它很是简单,只需在使用Go
命令行工具时添加-race
标志。例如,让咱们尝试使用-race
标志来运行咱们刚刚编写的程序:
go run -race main.go
执行后将输出:
0 ================== WARNING: DATA RACE Write at 0x00c00001a0a8 by goroutine 6: main.getNumber.func1() /QSC/go/src/example.com/http_demo/utils/vlog/main.go:12 +0x38 Previous read at 0x00c00001a0a8 by main goroutine: main.getNumber() /QSC/go/src/example.com/http_demo/utils/vlog/main.go:15 +0x88 main.main() /QSC/go/src/example.com/http_demo/utils/vlog/main.go:6 +0x33 Goroutine 6 (running) created at: main.getNumber() /QSC/go/src/example.com/http_demo/utils/vlog/main.go:11 +0x7a main.main() /QSC/go/src/example.com/http_demo/utils/vlog/main.go:6 +0x33 ================== Found 1 data race(s) exit status 66
第一个0是打印结果(所以咱们如今知道是操做2首先完成)。接下来的几行给出了在代码中检测到的数据竞争的信息。咱们能够看到关于数据竞争的信息分为三个部分:
getNumber
函数里建立的goroutine
中尝试写入(这是咱们将值5赋给i
的位置)goroutine
里有一个在同时进行的读操做。goroutine
是在哪里被建立的。除了go run
命令外,go build
和go test
命令也支持使用-race
标志。这个会使编译器建立的应用程序可以记录全部运行期间对共享变量访问,而且会记录下每个读或者写共享变量的goroutine
的身份信息。
竞争检查器会报告全部的已经发生的数据竞争。然而,它只能检测到运行时的竞争条件,并不能证实以后不会发生数据竞争。因为须要额外的记录,所以构建时加了竞争检测的程序跑起来会慢一些,且须要更大的内存,即便是这样,这些代价对于不少生产环境的工做来讲仍是能够接受的。对于一些偶发的竞争条件来讲,使用附带竞争检查器的应用程序能够节省不少花在Debug
上的时间。
Go
提供了不少解决它的选择。全部这些解决方案的思路都是确保在咱们写入变量时阻止对该变量的访问。通常经常使用的解决数据竞争的方案有:使用WaitGroup
锁,使用通道阻塞以及使用Mutex
锁,下面咱们一个个来看他们的用法并比较一下这几种方案的不一样点。
解决数据竞争的最直接方法是(若是需求容许的状况下)阻止读取访问,直到写入操做完成:
func getNumber() int { var i int // 初始化一个WaitGroup var wg sync.WaitGroup // Add(1) 通知程序有一个须要等待完成的任务 wg.Add(1) go func() { i = 5 // 调用wg.Done 表示正在等待的程序已经执行完成了 wg.Done() }() // wg.Wait会阻塞当前程序直到等待的程序都执行完成为止 wg.Wait() return i }
下面是使用WaitGroup
后程序执行的时间线:
这个方法原则上与上一种方法相似,只是咱们使用了通道而不是WaitGroup
:
func getNumber() int { var i int // 建立一个通道,在等待的任务完成时会向通道发送一个空结构体 done := make(chan struct{}) go func() { i = 5 // 执行完成后向通道发送一个空结构体 done <- struct{}{} }() // 从通道接收值将会阻塞程序,直到有值发送给done通道为止 <-done return i }
下图是使用通道阻塞解决数据竞争后程序的执行流程:
到目前为止,使用的解决方案只有在肯定写入操做完成后再读取i
的值时才适用。如今让咱们考虑一个更一般的状况,程序读取和写入的顺序并非固定的,咱们只要求它们不能同时发生就行。这种状况下咱们应该考虑使用Mutex
互斥锁。
// 首先,建立一个结构体包含咱们想用互斥锁保护的值和一个mutex实例 type SafeNumber struct { val int m sync.Mutex } func (i *SafeNumber) Get() int {、 i.m.Lock() defer i.m.Unlock() return i.val } func (i *SafeNumber) Set(val int) { i.m.Lock() defer i.m.Unlock() i.val = val } func getNumber() int { // 建立一个sageNumber实例 i := &SafeNumber{} // 使用Set和Get代替常规赋值和读取操做。 // 咱们如今能够确保只有在写入完成时才能读取,反之亦然 go func() { i.Set(5) }() return i.Get() }
下面两个图片对应于程序先获取到写锁和先获取到读锁两种可能的状况下程序的执行流程:
上面咱们使用互斥锁和通道两种方法解决了并发程序的数据竞争问题。那么咱们该在什么状况下使用互斥锁,什么状况下又该使用通道呢?答案就在你试图解决的问题中。若是你试图解决的问题更适合互斥锁,那么就继续使用互斥锁。。若是问题彷佛更适合渠道,则使用它。
大多数Go
新手都试图使用通道来解决全部并发问题,由于这是Go
语言的一个很酷的特性。这是不对的。语言为咱们提供了使用Mutex
或Channel
的选项,选择二者都没有错。
一般,当goroutine
须要相互通讯时使用通道,当确保同一时间只有一个goroutine
能访问代码的关键部分时使用互斥锁。在咱们上面解决的问题中,我更倾向于使用互斥锁,由于这个问题不须要goroutine
之间的任何通讯。只须要确保同一时间只有一个goroutine
拥有共享变量的使用权,互斥锁原本就是为解决这种问题而生的,因此使用互斥锁是更天然的一种选择。
上面讲数据竞争问题举的例子里由于多个goroutine
之间不须要通讯,因此使用Mutex
互斥锁的方案更合理些。那么针对使用Channel
的并发编程场景咱们就先留一道思考题给你们,题目以下:
假设有一个超长的切片,切片的元素类型为int,切片中的元素为乱序排列。限时5秒,使用多个goroutine查找切片中是否存在给定值,在找到目标值或者超时后马上结束全部goroutine的执行。好比切片为:[23, 32, 78, 43, 76, 65, 345, 762, ...... 915, 86],查找的目标值为345,若是切片中存在目标值程序输出:"Found it!"而且当即取消仍在执行查找任务的
goroutine
。若是在超时时间为找到目标值程序输出:"Timeout! Not Found",同时当即取消仍在执行查找任务的goroutine
。
不用顾忌题目里切片的元素重不重复,也不须要对切片元素进行排序。解决这个问题确定会用到context
、计时器、通道以及select
语句(已经提示了不少啦:),至关于把最近关于并发编程文章里的知识串一遍。
看文章的朋友们尽可能都想一想应该怎么解,在留言里说出大家的解题思路,最好能够私信我你写的代码的截图。我会在下周的文章里给出这个题目个人解决方法。这个题没有标准答案,只要能解出来而且思路值得借鉴我都会一块儿公布到下周的文章里。