Any race is a bug. When there is a race, the compiler is free to do whatever it wants.

实践

1)


u@u5:~/tmp$ cat g.go

package main

import (
"fmt"
"runtime"
"time"
)

var i = 0

func main() {
runtime.GOMAXPROCS(2)
go func() {
for {
fmt.Println("i is ", i)
time.Sleep(time.Second)
}
}()
for {
i += 1
}
}


u@u5:~/tmp$ go run -race g.go
==================
WARNING: DATA RACE
Read at 0x0000005d3400 by goroutine 7:
main.main.func1()
/home/u/tmp/g.go:16 +0x30

Previous write at 0x0000005d3400 by main goroutine:
main.main()
/home/u/tmp/g.go:21 +0x5c

Goroutine 7 (running) created at:
main.main()
/home/u/tmp/g.go:14 +0x35
==================
i is 63508
i is 19798494
i is 39143963
i is 58606456
i is 78955332
^Csignal: interrupt
u@u5:~/tmp$ go version
go version go1.17 linux/amd64
u@u5:~/tmp$

 

 

https://mp.weixin.qq.com/s/pVJiFdDDKVx707eKL19bjA

 

谈谈 Golang 中的 Data Race

Any race is a bug

我在接手其他同事的 golang 项目时,一般都会习惯性的做一个竞态检测。有时总会得到一些“惊喜”,比如像下面这段代码:

 
package main

import (
	"fmt"
	"runtime"
	"time"
)

var i = 0

func main() {
	runtime.GOMAXPROCS(2)
	go func() {
		for {
			fmt.Println("i is ", i)
			time.Sleep(time.Second)
		}
	}()
	for {
		i += 1
	}
}

  

当通过 go run-race cmd.go 执行时,可以看到有明显的竞态出现:


 go run -race g.go
==================
WARNING: DATA RACE
Read at 0x0000006459c0 by goroutine 7:
  main.main.func1()
      /home/s/goAction/tDataRace/g.go:15 +0x3e

Previous write at 0x0000006459c0 by main goroutine:
  main.main()
      /home/s/goAction/tDataRace/g.go:20 +0x84

Goroutine 7 (running) created at:
  main.main()
      /home/s/goAction/tDataRace/g.go:13 +0x53
==================
i is  380899
i is  33836429

  

我觉得不同的 goroutine 并发读写同一个变量,需要加锁,这应该是天经地义的常识。但是总有人以为,不加锁导致的问题最多就是读取的数据是修改前的数据,不能保证原子性罢了。是这样的吗?从上面的输出来看,似乎也差不多,其实这些都是典型的误解。

有些朋友可能不知道,在 Go(甚至是大部分语言)中,一条普通的赋值语句其实并不是一个原子操作(语言规范同样没有定义 i++ 是原子操作, 任何变量的赋值都不是原子操作)。例如,在 32 位机器上写 int64类型的变量是有中间状态的,它会被拆成两次写操作 MOV —— 写低 32 位和写高 32 位,如下图所示:

 

 

如果一个线程刚写完低 32 位,还没来得及写高 32 位时,另一个线程读取了这个变量,那它得到的就是一个毫无逻辑的中间变量,这很有可能使我们的程序出现诡异的 Bug。

而在 Go 的内存模型中,有 race 的 Go 程序的行为是未定义行为,理论上出现什么情况都是正常的。就拿上面的代码来说,当去掉 -race 参数执行时,大概率会得到这样的输出:

  1. i is: 0

  2. i is: 0

  3. i is: 0

  4. i is: 0

而用较老的 go 版本执行时,基本上执行一段时间,程序就会 HANG 住。所以讨论为什么出现这种现象实际上没有任何意义,不要依赖这种行为。

Mutex vs Atomic

解决 race 的问题时,无非就是上锁。可能很多人都听说过一个高逼格的词叫「无锁队列」。 都一听到加锁就觉得很 low,那无锁又是怎么一回事?其实就是利用 atomic 特性,那 atomic 会比 mutex 有什么好处呢?Benign Data Races: What Could Possibly Go Wrong? 的作者总结了这两者的一个区别:

Mutexes do no scale. Atomic loads do.

mutex操作系统实现,而 atomic 包中的原子操作则由底层硬件直接提供支持。在 CPU 实现的指令集里,有一些指令被封装进了 atomic 包,这些指令在执行的过程中是不允许中断(interrupt)的,因此原子操作可以在 lock-free 的情况下保证并发安全,并且它的性能也能做到随 CPU 个数的增多而线性扩展。

若实现相同的功能,后者通常会更有效率,并且更能利用计算机多核的优势。所以,以后当我们想并发安全的更新一些变量的时候,我们应该优先选择用 atomic 来实现。

参考资料

  • The Go Memory Model

  • Would this race condition be considered a bug?

  • 理解Go标准库中的atomic.Value类型

 

[s@s tDataRace]$ ll -as
total 2000
   4 drwxr-xr-x  2 s go    4096 Dec 21 08:11 .
   4 drwxr-xr-x 13 s go    4096 Dec 21 08:03 ..
1988 -rwxr-xr-x  1 s go 2035035 Dec 21 08:11 g
   4 -rw-r--r--  1 s go     200 Dec 21 08:05 g.go
[s@s tDataRace]$ rm -rf g
[s@s tDataRace]$ go build g.go
[s@s tDataRace]$ ll -as
total 2000
   4 drwxr-xr-x  2 s go    4096 Dec 21 08:11 .
   4 drwxr-xr-x 13 s go    4096 Dec 21 08:03 ..
1988 -rwxr-xr-x  1 s go 2035035 Dec 21 08:11 g
   4 -rw-r--r--  1 s go     200 Dec 21 08:05 g.go
[s@s tDataRace]$ go tool objdump -s main.main g
TEXT main.main(SB) /home/s/goAction/tDataRace/g.go
  g.go:11               0x4992e0                64488b0c25f8ffffff      MOVQ FS:                                                                   0xfffffff8, CX
  g.go:11               0x4992e9                483b6110                CMPQ 0x1                                                                   0(CX), SP
  g.go:11               0x4992ed                7639                    JBE 0x49                                                                   9328
  g.go:11               0x4992ef                4883ec18                SUBQ $0x                                                                   18, SP
  g.go:11               0x4992f3                48896c2410              MOVQ BP,                                                                    0x10(SP)
  g.go:11               0x4992f8                488d6c2410              LEAQ 0x1                                                                   0(SP), BP
  g.go:12               0x4992fd                48c7042402000000        MOVQ $0x                                                                   2, 0(SP)
  g.go:12               0x499305                e876d3f6ff              CALL run                                                                   time.GOMAXPROCS(SB)
  g.go:13               0x49930a                c7042400000000          MOVL $0x                                                                   0, 0(SP)
  g.go:13               0x499311                488d0598b70200          LEAQ 0x2                                                                   b798(IP), AX
  g.go:13               0x499318                4889442408              MOVQ AX,                                                                    0x8(SP)
  g.go:13               0x49931d                0f1f00                  NOPL 0(A                                                                   X)
  g.go:13               0x499320                e83b3afaff              CALL run                                                                   time.newproc(SB)
  g.go:19               0x499325                90                      NOPL
  g.go:1                0x499326                ebfd                    JMP 0x49                                                                   9325
  g.go:11               0x499328                e87386fcff              CALL run                                                                   time.morestack_noctxt(SB)
  g.go:11               0x49932d                ebb1                    JMP main                                                                   .main(SB)

TEXT main.main.func1(SB) /home/s/goAction/tDataRace/g.go
  g.go:13               0x499340                64488b0c25f8ffffff      MOVQ FS:                                                                   0xfffffff8, CX
  g.go:13               0x499349                483b6110                CMPQ 0x1                                                                   0(CX), SP
  g.go:13               0x49934d                0f86a4000000            JBE 0x49                                                                   93f7
  g.go:13               0x499353                4883ec68                SUBQ $0x                                                                   68, SP
  g.go:13               0x499357                48896c2460              MOVQ BP,                                                                    0x60(SP)
  g.go:13               0x49935c                488d6c2460              LEAQ 0x6                                                                   0(SP), BP
  g.go:15               0x499361                488b0598af0e00          MOVQ mai                                                                   n.i(SB), AX
  g.go:15               0x499368                48890424                MOVQ AX,                                                                    0(SP)
  g.go:15               0x49936c                e86f0df7ff              CALL run                                                                   time.convT64(SB)
  g.go:15               0x499371                488b442408              MOVQ 0x8                                                                   (SP), AX
  g.go:15               0x499376                0f57c0                  XORPS X0                                                                   , X0
  g.go:15               0x499379                0f11442440              MOVUPS X                                                                   0, 0x40(SP)
  g.go:15               0x49937e                0f11442450              MOVUPS X                                                                   0, 0x50(SP)
  g.go:15               0x499383                488d0d16b50000          LEAQ 0xb                                                                   516(IP), CX
  g.go:15               0x49938a                48894c2440              MOVQ CX,                                                                    0x40(SP)
  g.go:15               0x49938f                488d157a170400          LEAQ 0x4                                                                   177a(IP), DX
  g.go:15               0x499396                4889542448              MOVQ DX,                                                                    0x48(SP)
  g.go:15               0x49939b                488d1d7eae0000          LEAQ 0xa                                                                   e7e(IP), BX
  g.go:15               0x4993a2                48895c2450              MOVQ BX,                                                                    0x50(SP)
  g.go:15               0x4993a7                4889442458              MOVQ AX,                                                                    0x58(SP)
  print.go:274          0x4993ac                488b05fdb50b00          MOVQ os.                                                                   Stdout(SB), AX
  print.go:274          0x4993b3                488d35662d0400          LEAQ go.                                                                   itab.*os.File,io.Writer(SB), SI
  print.go:274          0x4993ba                48893424                MOVQ SI,                                                                    0(SP)
  print.go:274          0x4993be                4889442408              MOVQ AX,                                                                    0x8(SP)
  print.go:274          0x4993c3                488d442440              LEAQ 0x4                                                                   0(SP), AX
  print.go:274          0x4993c8                4889442410              MOVQ AX,                                                                    0x10(SP)
  print.go:274          0x4993cd                48c744241802000000      MOVQ $0x                                                                   2, 0x18(SP)
  print.go:274          0x4993d6                48c744242002000000      MOVQ $0x                                                                   2, 0x20(SP)
  print.go:274          0x4993df                90                      NOPL
  print.go:274          0x4993e0                e85b9affff              CALL fmt                                                                   .Fprintln(SB)
  g.go:16               0x4993e5                48c7042400ca9a3b        MOVQ $0x                                                                   3b9aca00, 0(SP)
  g.go:16               0x4993ed                e86e72fcff              CALL tim                                                                   e.Sleep(SB)
  g.go:15               0x4993f2                e96affffff              JMP 0x49                                                                   9361
  g.go:13               0x4993f7                e8a485fcff              CALL run                                                                   time.morestack_noctxt(SB)
  g.go:13               0x4993fc                0f1f4000                NOPL 0(A                                                                   X)
  g.go:13               0x499400                e93bffffff              JMP main                                                                   .main.func1(SB)
[s@s tDataRace]$ go build -race g.go
[s@s tDataRace]$ go tool objdump -s main.main g
TEXT main.main(SB) /home/s/goAction/tDataRace/g.go
  g.go:11               0x512ba0                64488b0c25f8ffffff      MOVQ FS:0xfffffff8, CX
  g.go:11               0x512ba9                483b6110                CMPQ 0x10(CX), SP
  g.go:11               0x512bad                0f8683000000            JBE 0x512c36
  g.go:11               0x512bb3                4883ec20                SUBQ $0x20, SP
  g.go:11               0x512bb7                48896c2418              MOVQ BP, 0x18(SP)
  g.go:11               0x512bbc                488d6c2418              LEAQ 0x18(SP), BP
  g.go:11               0x512bc1                488b442420              MOVQ 0x20(SP), AX
  g.go:11               0x512bc6                48890424                MOVQ AX, 0(SP)
  g.go:11               0x512bca                e8315cf8ff              CALL runtime.racefuncenter(SB)
  g.go:12               0x512bcf                48c7042402000000        MOVQ $0x2, 0(SP)
  g.go:12               0x512bd7                e8e454f2ff              CALL runtime.GOMAXPROCS(SB)
  g.go:13               0x512bdc                c7042400000000          MOVL $0x0, 0(SP)
  g.go:13               0x512be3                488d050e310300          LEAQ 0x3310e(IP), AX
  g.go:13               0x512bea                4889442408              MOVQ AX, 0x8(SP)
  g.go:13               0x512bef                e88cc2f5ff              CALL runtime.newproc(SB)
  g.go:20               0x512bf4                488d05c52d1300          LEAQ main.i(SB), AX
  g.go:20               0x512bfb                48890424                MOVQ AX, 0(SP)
  g.go:20               0x512bff                90                      NOPL
  g.go:20               0x512c00                e8db5af8ff              CALL runtime.raceread(SB)
  g.go:20               0x512c05                488b05b42d1300          MOVQ main.i(SB), AX
  g.go:20               0x512c0c                4889442410              MOVQ AX, 0x10(SP)
  g.go:20               0x512c11                488d0da82d1300          LEAQ main.i(SB), CX
  g.go:20               0x512c18                48890c24                MOVQ CX, 0(SP)
  g.go:20               0x512c1c                0f1f4000                NOPL 0(AX)
  g.go:20               0x512c20                e8fb5af8ff              CALL runtime.racewrite(SB)
  g.go:20               0x512c25                488b442410              MOVQ 0x10(SP), AX
  g.go:20               0x512c2a                48ffc0                  INCQ AX
  g.go:20               0x512c2d                4889058c2d1300          MOVQ AX, main.i(SB)
  g.go:20               0x512c34                ebbe                    JMP 0x512bf4
  g.go:11               0x512c36                e8a526f8ff              CALL runtime.morestack_noctxt(SB)
  g.go:11               0x512c3b                0f1f440000              NOPL 0(AX)(AX*1)
  g.go:11               0x512c40                e95bffffff              JMP main.main(SB)

TEXT main.main.func1(SB) /home/s/goAction/tDataRace/g.go
  g.go:13               0x512c60                64488b0c25f8ffffff      MOVQ FS:0xfffffff8, CX
  g.go:13               0x512c69                483b6110                CMPQ 0x10(CX), SP
  g.go:13               0x512c6d                0f86d2000000            JBE 0x512d45
  g.go:13               0x512c73                4883ec68                SUBQ $0x68, SP
  g.go:13               0x512c77                48896c2460              MOVQ BP, 0x60(SP)
  g.go:13               0x512c7c                488d6c2460              LEAQ 0x60(SP), BP
  g.go:13               0x512c81                488b442468              MOVQ 0x68(SP), AX
  g.go:13               0x512c86                48890424                MOVQ AX, 0(SP)
  g.go:13               0x512c8a                e8715bf8ff              CALL runtime.racefuncenter(SB)
  g.go:15               0x512c8f                488d052a2d1300          LEAQ main.i(SB), AX
  g.go:15               0x512c96                48890424                MOVQ AX, 0(SP)
  g.go:15               0x512c9a                e8415af8ff              CALL runtime.raceread(SB)
  g.go:15               0x512c9f                488b051a2d1300          MOVQ main.i(SB), AX
  g.go:15               0x512ca6                48890424                MOVQ AX, 0(SP)
  g.go:15               0x512caa                e8b18ef2ff              CALL runtime.convT64(SB)
  g.go:15               0x512caf                488b442408              MOVQ 0x8(SP), AX
  g.go:15               0x512cb4                0f57c0                  XORPS X0, X0
  g.go:15               0x512cb7                0f11442440              MOVUPS X0, 0x40(SP)
  g.go:15               0x512cbc                0f11442450              MOVUPS X0, 0x50(SP)
  g.go:15               0x512cc1                488d0d58c40000          LEAQ 0xc458(IP), CX
  g.go:15               0x512cc8                48894c2440              MOVQ CX, 0x40(SP)
  g.go:15               0x512ccd                488d151ce20400          LEAQ 0x4e21c(IP), DX
  g.go:15               0x512cd4                4889542448              MOVQ DX, 0x48(SP)
  g.go:15               0x512cd9                488d1d40bd0000          LEAQ 0xbd40(IP), BX
  g.go:15               0x512ce0                48895c2450              MOVQ BX, 0x50(SP)
  g.go:15               0x512ce5                4889442458              MOVQ AX, 0x58(SP)
  print.go:274          0x512cea                488d057f331000          LEAQ os.Stdout(SB), AX
  print.go:274          0x512cf1                48890424                MOVQ AX, 0(SP)
  print.go:274          0x512cf5                e8e659f8ff              CALL runtime.raceread(SB)
  print.go:274          0x512cfa                488b056f331000          MOVQ os.Stdout(SB), AX
  print.go:274          0x512d01                488d0db8fa0400          LEAQ go.itab.*os.File,io.Writer(SB), CX
  print.go:274          0x512d08                48890c24                MOVQ CX, 0(SP)
  print.go:274          0x512d0c                4889442408              MOVQ AX, 0x8(SP)
  print.go:274          0x512d11                488d442440              LEAQ 0x40(SP), AX
  print.go:274          0x512d16                4889442410              MOVQ AX, 0x10(SP)
  print.go:274          0x512d1b                48c744241802000000      MOVQ $0x2, 0x18(SP)
  print.go:274          0x512d24                48c744242002000000      MOVQ $0x2, 0x20(SP)
  print.go:274          0x512d2d                e88e60ffff              CALL fmt.Fprintln(SB)
  g.go:16               0x512d32                48c7042400ca9a3b        MOVQ $0x3b9aca00, 0(SP)
  g.go:16               0x512d3a                e8a111f8ff              CALL time.Sleep(SB)
  g.go:16               0x512d3f                90                      NOPL
  g.go:15               0x512d40                e94affffff              JMP 0x512c8f
  g.go:13               0x512d45                e89625f8ff              CALL runtime.morestack_noctxt(SB)
  g.go:13               0x512d4a                e911ffffff              JMP main.main.func1(SB)
[s@s tDataRace]$

  

当通过 go run cmd.go 执行时,大概率会得到下面这样的输出:

  1. i is: 0

  2. i is: 0

  3. i is: 0

  4. i is: 0

然而有些同学提到:之所以输出 0 是因为 i+=1 所在的 goroutine 没有新的栈帧创建,因此没有被调度器调度到。解释似乎也合理,但是事实却不是这样的。真实的原因是:编译器把那段自增的 for 循环给全部优化掉了

要验证这一点,我们要先从编译器优化说起。传统的编译器通常分为三个部分,前端(frontEnd),优化器(Optimizer)和后端(backEnd)。在编译过程中,前端主要负责词法和语法分析,将源代码转化为抽象语法树;优化器则是在前端的基础上,对得到的中间代码进行优化,使代码更加高效;后端则是将已经优化的中间代码转化为针对各自平台的机器代码。

go 的编译器也一样,在生成目标代码的时候会做很多优化,重要的有:

  • 指令重排

  • 逃逸分析

  • 函数内联

  • 死码消除

查看编译出的二进制可执行文件的汇编代码:

 

显然,下面这一段直接被优化没了:

  1. for {

  2. i += 1

  3. }

why? 因为这段代码是有竞态的,没有任何同步机制。go 编译器认为这一段是 dead code,索性直接优化掉了。

而当我们通过 go build-race g.go 编译后:


 
可以明显看到有 INCQ 指令了,这是因为 -race 选项打开了 data race detector 用来检查这个错误而关闭了相关的编译器优化:
 
 

此,运行结果就“看似正确”了。

最后再引用一句 golang-nuts 上的评论:

Any race is a bug. When there is a race, the compiler is free to do whatever it wants.

 

参考资料

  • Go compiler - Loop transformations

  • Would this race condition be considered a bug?

Spin-Locks vs. Atomic Instructions - Intel Community https://community.intel.com/t5/Intel-oneAPI-Threading-Building/Spin-Locks-vs-Atomic-Instructions/td-p/894992

c++ - Why is std::mutex faster than std::atomic? - Stack Overflow https://stackoverflow.com/questions/29533755/why-is-stdmutex-faster-than-stdatomic

Benign Data Races: What Could Possibly Go Wrong? https://software.intel.com/content/www/us/en/develop/blogs/benign-data-races-what-could-possibly-go-wrong.html

Comparing the performance of atomic, spinlock and mutex https://demin.ws/blog/english/2012/05/05/atomic-spinlock-mutex/

 

 

https://tour.go-zh.org/concurrency/9

 

sync.Mutex

我们已经看到信道非常适合在各个 Go 程间进行通信。

但是如果我们并不需要通信呢?比如说,若我们只是想保证每次只有一个 Go 程能够访问一个共享的变量,从而避免冲突?

这里涉及的概念叫做 *互斥(mutual*exclusion)* ,我们通常使用 *互斥锁(Mutex)* 这一数据结构来提供这种机制。

Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:

  • Lock
  • Unlock

我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。参见 Inc 方法。

我们也可以用 defer 语句来保证互斥锁一定会被解锁。参见 Value 方法。

package main

import (
	"fmt"
	"sync"
	"time"
)

// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
	v   map[string]int
	mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
	c.mux.Lock()
	// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
	c.v[key]++
	c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
	c.mux.Lock()
	// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
	defer c.mux.Unlock()
	return c.v[key]
}

func main() {
	c := SafeCounter{v: make(map[string]int)}
	for i := 0; i < 1000; i++ {
		go c.Inc("somekey")
	}

	time.Sleep(time.Second)
	fmt.Println(c.Value("somekey"))
}

  

 

// 2021/1/4 20:50 Shawn
package main

import (
	"fmt"
	"time"
)

type UnSafeCounter struct {
	v map[string]int
}

func (c *UnSafeCounter) Inc(key string) {
	c.v[key]++
	time.Sleep(10 * time.Millisecond)
}
func (c *UnSafeCounter) Value(key string) int {
	return c.v[key]
}

func main() {
	c := UnSafeCounter{v: make(map[string]int)}
	for i := 0; i < 1000; i++ {
		go c.Inc("somekey")
	}
	time.Sleep(time.Second)
	fmt.Println(c.Value("somekey"))
}

  

 

go run -race g.go
1000

 

go run -race g.go
==================
WARNING: DATA RACE
Read at 0x00c000078150 by goroutine 778:
runtime.mapaccess1_faststr()
/home/shawn/go/src/runtime/map_faststr.go:12 +0x0
main.(*UnSafeCounter).Inc()
/home/shawn/gokit/goAction/tMutex2/g.go:14 +0x74

Previous write at 0x00c000078150 by goroutine 1005:
runtime.mapassign_faststr()
/home/shawn/go/src/runtime/map_faststr.go:202 +0x0
main.(*UnSafeCounter).Inc()
/home/shawn/gokit/goAction/tMutex2/g.go:14 +0xc4

Goroutine 778 (running) created at:
main.main()
/home/shawn/gokit/goAction/tMutex2/g.go:24 +0xb9

Goroutine 1005 (running) created at:
main.main()
/home/shawn/gokit/goAction/tMutex2/g.go:24 +0xb9
==================
==================
WARNING: DATA RACE
Read at 0x00c00007a978 by goroutine 778:
main.(*UnSafeCounter).Inc()
/home/shawn/gokit/goAction/tMutex2/g.go:14 +0x87

Previous write at 0x00c00007a978 by goroutine 1005:
main.(*UnSafeCounter).Inc()
/home/shawn/gokit/goAction/tMutex2/g.go:14 +0xd9

Goroutine 778 (running) created at:
main.main()
/home/shawn/gokit/goAction/tMutex2/g.go:24 +0xb9

Goroutine 1005 (running) created at:
main.main()
/home/shawn/gokit/goAction/tMutex2/g.go:24 +0xb9
==================
1000
Found 2 data race(s)
exit status 66

 

在 Go 中发现竞态条件 (Race Conditions)

magichan · 2018-10-09 15:57:23 · 4394 次点击 · 预计阅读时间 9 分钟 · 大约1分钟之前 开始浏览    
这是一个创建于 2018-10-09 15:57:23 的文章,其中的信息可能已经有所发展或是发生改变。

当我意识到我一直在处理和解决的问题有一个专有名词描述的时候,我总会觉得这事十分有趣。这次出现这种情况的是竞争条件(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 开发人员能有这样的工具是很幸运地,所以值得学习一下。


via: https://www.ardanlabs.com/blog/2013/09/detecting-race-conditions-with-go.html

作者:William Kennedy  译者:magichan  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出

在 Go 中发现竞态条件 (Race Conditions) - Go语言中文网 - Golang中文社区 https://studygolang.com/articles/15349

 

 

 

 

 

posted @ 2020-06-12 01:07  papering  阅读(246)  评论(0编辑  收藏  举报