golang data race 竞态条件

Posted 惜暮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang data race 竞态条件相关的知识,希望对你有一定的参考价值。

golang race condition 竞态条件

golang的协程机制使得编写并发代码变得非常容易,但是多线程(协程)编程离不开的一个话题就是线程(协程)安全,也就是并发环境下是不是存在竞态条件。

这里有两个概念首先需要区分清楚:竞态条件(race condition) 和 数据竞争 (data race)。

data race

定义:①多个线程(协程)对于同一个变量、②同时地、③进行读/写操作、并且④至少有一个线程进行写操作。(也就是说,如果所有线程都是只进行读操作,那么将不构成数据争用)

后果:如果发生了数据争用,读取该变量时得到的值将变得不可知(根据内存模型),使得该多线程程序的运行结果将完全不可预测,有一定可能会导致直接崩溃。

如何防止:对于有可能被多个线程同时访问的变量使用排他访问控制,具体方法包括使用mutex(互斥量)或者使用atomic变量。

race condition

相对于数据争用(data race),竞态条件(race condition)指的是更加高层次的更加复杂的现象,一般需要在设计并行程序时进行细致入微的分析,才能确定。(也就是隐藏得更深)

定义:受各线程上代码执行的顺序和时机的影响,程序的运行结果产生(预料之外)的变化。

后果:如果存在竞态条件(race condition),多次运行程序对于同一个输入将会有不同的结果,但结果并非完全不可预测,它将由输入数据和各线程的执行顺序共同决定。

如何预防:竞态条件产生的原因很多是对于同一个资源的一系列连续操作并不是原子性的,也就是说有可能在执行的中途被其他线程抢占,同时这个“其他线程”刚好也要访问这个资源。解决方法通常是:将这一系列操作作为一个critical section(临界区)。

golang race detector

golang 提供了race检测工具,这个竞态检测工具是在编译流程中内置检测代码到应用程序。一旦你的程序开始运行,它能够发现和报告任何他所检测到的竞态情况。 这是非常棒的功能, 为识别缺陷代码做出了非常重要的工作。

下面从实例出发来看data race:

package main

import (
	"fmt"
	"sync"
	"testing"
)

func Test_normal(t *testing.T)  
	Wait := &sync.WaitGroup
	var Counter = 0
	Wait.Add(2)
	
	for routine := 1; routine <= 2; routine++ 
		go Routine(Wait, &Counter)
	
	Wait.Wait()
	fmt.Printf("Final Counter: %d\\n", Counter)


func Routine(Wait *sync.WaitGroup, Counter *int) 
	for count := 0; count < 2; count++ 
		value := *Counter
		value++
		*Counter = value
	
	Wait.Done()

这个程序很简单,并发的执行:

value := *Counter //读
value++
*Counter = value//写

这里很明显有并发问题,变量 *Counter 存在并发读写的情况。

我们跑一下 go test -race 结果如下:

==================
WARNING: DATA RACE
Read at 0x00c0000a4080 by goroutine 10:
  command-line-arguments.Routine()
      /Users/ytlou/Desktop/golang/golang_study/study/basic/race/data_race1_test.go:23 +0x47

Previous write at 0x00c0000a4080 by goroutine 9:
  command-line-arguments.Routine()
      /Users/ytlou/Desktop/golang/golang_study/study/basic/race/data_race1_test.go:25 +0x5d

Goroutine 10 (running) created at:
  command-line-arguments.Test_normal()
      /Users/ytlou/Desktop/golang/golang_study/study/basic/race/data_race1_test.go:15 +0xef
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:909 +0x199

Goroutine 9 (finished) created at:
  command-line-arguments.Test_normal()
      /Users/ytlou/Desktop/golang/golang_study/study/basic/race/data_race1_test.go:15 +0xef
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:909 +0x199
==================
Final Counter: 4
--- FAIL: Test_normal (0.00s)
    testing.go:853: race detected during execution of test
FAIL
FAIL    command-line-arguments  0.848s
FAIL

提示很友好的告诉了存在并发读写的情况。但是我们实际运行这个代码,结果却在大部分情况下都是正确的,输出4,这也是并发问题难以排查的地方。

这里假设输出是2,我们看一下协程执行流程:(主内存和协程内存之间的关系背景假设已知)

上面没有画全,但是已经能够说明问题,一句话说明就是:没有保证有并发读写情况下的排他性。

即使将上面核心的读写改成一行:

*Counter = *Counter + 1

但是这一行在编译器编译之后也是好几个机器指令,所以并发问题并不会解决。

所以解决的办法就是:

Lock.Lock()
*Counter = *Counter + 1
Lock.Unlock()

保证排他。没有发现任何竞态条件。

这时候 go test race 就能够pass。

以上是关于golang data race 竞态条件的主要内容,如果未能解决你的问题,请参考以下文章

Data Race Detector for Golang

多线程系列Race Condition的产生和解决

JUC并发编程 -- 共享代来的问题(操作统一资源) & 临界区 Critical Section & 竞态条件 Race Condition

Golang Data Race Detector

临界区与竞态条件

promiseall退出条件