当两个goroutine同时访问同一个变量,且至少一次访问是写操作时,就会发生数据竞争。数据竞争很常见,也很难调试。以下函数存在数据竞争情况,其输出结果是不确定的。可以先仔细阅读代码,尝试着看看究竟发生了什么。

func race(){

n:= 0

go func() { // goroutine g1

fmt.Println("goroute1 - ", rand.Intn(10))

n+= 5 //read,increment,write

}()

n++//数据访问竞争

fmt.Println("gomain - ", rand.Intn(10))

fmt.Println(n)//Output:<unspecified>

}

由于两个goroutine,main和g1,之间存在数据竞争,所以无法知道操作将以什么顺序发生。“数据竞争”这个名称其实有点误导,实际上不仅操作的顺序没有定义,而且几乎没有什么保证。为了获得更好的性能,编译器和硬件经常将代码颠倒过来并由内而外执行。

 

如何避免数据竞争

避免数据争用的唯一方法是同步对线程间共享的所有可变数据的访问。有几种方法可以做到这一点。在Go中一般使用通道或锁技术。(注:在sync和sync/atomic包中提供了较低级别的锁机制)

 

在Go中处理并发数据访问的首选方法是使用一个通道将实际数据从一个goroutine传递到下一个goroutine。Go的哲学是:“不要通过分享内存来通信,应当通过通信分享内存。”

func sharingIsCaring(){

ch:= make(chan int)

go func() {

fmt.Println("gofunc route1 - ", rand.Intn(10))

n:= 0 //闭包内的局部变量仅goroutine内部可见

n+= 5

ch<-n //数据通过通道发送

}()

n:= <-ch//数据通过通道安全到达main线程

n++

fmt.Println("gofunc main - ", rand.Intn(10))

fmt.Println(n)//Output:6

}

在上面这段代码中,通道包含了两个作用:

  • 它将数据从一个goroutine传递到另一个goroutine。

  • 它扮演了一个同步点的角色。

发送数据的goroutine将等待(阻塞)直到另一个goroutine接收数据;而接收数据的goroutine将等待(阻塞)直到另一个goroutine发送数据到通道中。

 

数据竞争很容易发生,而且很难调试。幸运的是,Go运行时能够提供工具帮助开发者进行检测。通常可以使用-race来启用内置的数据竞争检测器。例如使用以下命令启动或测试程序:

$ go test -race [packages] //执行单元测试并开启竞态检测
$ go run -race [packages] //执行程序并开启竞态检测

 

数据竞争检测示例

以下是另一个数据竞争的程序:

package main
import "fmt"
func main() {
    i := 0
    go func() {
        i++// write
    }()
    fmt.Println(i)// concurrent read
}

使用-race选项运行这个程序将显示在第7行写和第9行读之间有一个竞争状态发生:

$ go run -race main.go
0
==================
WARNING: DATA RACE
Write by goroutine 6:
main.main.func1()
/tmp/main.go:7 +0x44
Previous read by main goroutine:
main.main()
/tmp/main.go:9 +0x7e
Goroutine 6 (running) created at:
main.main()
      /tmp/main.go:8 +0x70
==================
Found 1 data race(s)

exit status 66

 

数据竞争检测的代价

数据竞争检测器不执行任何静态分析。它在运行时检查内存访问,并且只检查实际执行的代码路径。它运行在darwin/amd64、freebsd/amd64、linux/amd64和windows/amd64上。启用竞争检测后根据不同系统情况开销略有不同,但通常内存使用量会增加5-10倍,执行时间增加2-20倍。

 

死锁的检测

再来看看另一种相关的场景——死锁。死锁发生在一组goroutine彼此等待而没有任何一个线程能够继续时。先看以下示例:

func main() {
ch := make(chan int)
ch<- 1
fmt.Println(<-ch)
}

程序将被卡在通道发送的操作,始终阻塞着等待有人读取数据。Go能够在运行时检测到这种情况。下面是程序的输出:

fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:

main.main()

.../deadlock.go:7 +0x6c

 

一般来说,可能会由于以下原因导致阻塞:

  • 从一个通道中等待数据

  • 等待同步包中的某个锁

 

更常见的原因是:

  • 没有其他goroutine可以访问通道或锁,始终处于阻塞状态。

  • 多组goroutines在互相等待,没有任何线程能够继续执行。

 

目前Go运行时只会在程序整体阻塞时检测到,而针对某个goroutine子集处于阻塞状态时无能为力。因此,对于通道,通常很容易找出导致死锁的原因;而另一方面,对于大量使用互斥锁的程序,调试起来会非常困难。

 

原文作者:yourbasic.org    译者:江玮