Go Data race and Vector clock

Go Data race and Vector clock

Data race 数据竞态(争)

data race是指多个运行分支(线程)同时访问某共享资源,并且有写操作的情况。此时该共享资源因同时发生读写,其数据状态不确定。编码时应杜绝数据竞态问题的产生。

如下是一个简单的例子,global 全局变量初始状态为0,我们启动两个协程,同时对global进行自增1的操作,各执行1万次。我们的假想是该代码执行完打印global时,值为20000。但并非如此,可能偶尔是20000。当前运行结果:16449。

package main

import (
	"fmt"
	"time"
)

var global = 0

func main() {
	go func() {
		for i := 0; i < 10000; i++ {
			global++
		}
	}()

	for i := 0; i < 10000; i++ {
		global++
	}
	time.Sleep(time.Second)
	fmt.Println(global)
}

造成这种问题的根源是对global的自增操作不具有原子性。并且有多个协程对没有锁保护的资源进行访问。

使用 -race 子命令检测数据竞争

这里使用go run -race main.go来检测main.go文件在执行过程中是否有竞态问题。-race 不仅可以用在 run 子命令后,还可用在 build , test 之后。

...\main>go run -race main.go
==================
WARNING: DATA RACE                                          
Read at 0x000000638610 by goroutine 7:                      
  main.main.func1()                                         
      .../main/main.go:13 +0x32
                                                            
Previous write at 0x000000638610 by main goroutine:         
  main.main()                                               
      .../main/main.go:18 +0x5d
                                                            
Goroutine 7 (running) created at:                           
  main.main()                                               
      .../main/main.go:11 +0x30
==================                                          
20000
Found 1 data race(s)
exit status 66

可以看出,-race子命令报告了代码Found 1 data race(s),并且标记了代码发生数竞争的位置。

data race 解决办法

当你的代码存在数据竞争时,一定要解决它。解决办法有几种:1. 对共享资源加锁;2.操作共享资源时使用原子操作(适用范围比较小);3.使用通道。其目的都是保证读写不在同一时刻发生,产生互斥的效应。

加锁
package main

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

var global = 0
var globalMu = sync.Mutex{}

func main() {
	go func() {
		for i := 0; i < 10000; i++ {
			globalMu.Lock()
			global++
			globalMu.Unlock()
		}
	}()

	for i := 0; i < 10000; i++ {
		globalMu.Lock()
		global++
		globalMu.Unlock()
	}
	time.Sleep(time.Second)
	fmt.Println(global)
}

再次使用go run -race main.go,检测运行中代码是否存在竞态问题:

...\main>go run -race main.go
20000

可以看出,没有竞态问题的发生了。

原子操作

此时将global修改为int64类型。

package main

import (
	"fmt"
	"sync/atomic"
	"time"
)

var global int64 = 0

func main() {
	go func() {
		for i := 0; i < 10000; i++ {
			atomic.AddInt64(&global, 1)
		}
	}()

	for i := 0; i < 10000; i++ {
		atomic.AddInt64(&global, 1)
	}
	time.Sleep(time.Second)
	fmt.Println(global)
}
通道
package main

import (
	"fmt"
	"time"
)

var global int64 = 0

func main() {
	ch := make(chan struct{}, 1)
	go func() {

		for i := 0; i < 10000; i++ {
			ch <- struct{}{}
			global++
			<-ch
		}
	}()

	for i := 0; i < 10000; i++ {
		ch <- struct{}{}
		global++
		<-ch
	}

	time.Sleep(time.Second)
	fmt.Println(global)
	close(ch)
}

以上办法都是利用同步机制来防止同一时刻读写数据的问题的发生。使读写操作满足happened-before原则。

Vector clock

A vector clock is a data structure used for determining the partial ordering of events in a distributed system and detecting causality violations.
向量时钟是一种数据结构,用于确定分布式系统中事件的部分排序并检测因果关系违规。
Just as in Lamport timestamps, inter-process messages contain the state of the sending process’s logical clock.
就像在Lamport时间戳一样,过程间消息包含发送过程的逻辑时钟的状态。
A vector clock of a system of N processes is an array/vector of N logical clocks, one clock per process; a local “largest possible values” copy of the global clock-array is kept in each process.
n个过程系统的向量时钟是n个逻辑时钟的数组/向量,每个过程一个时钟;在每个过程中保存全局时钟阵列的本地“最大值”副本。

以上文字来自维基英文百科。

Go race 检测原理

Go 语言内置的race检测工具为Google的Threadsanitizer,其内部原理即为Vector clock(矢量时钟)。Go语言在其源码中插入了大量的race相关代码,这些探针在需要race检测时被触发,同时,需要race检测时,编译器还会向可能发生data race的用户代码中插入探针。这些探针维护着矢量时钟的状态,从而感知到race的发生。由于插入探针,所以需要race检测的代码运行效率会更低,且内内存占用会更大。

Reference
https://en.wikipedia.org/wiki/Vector_clock
《Go 语言底层原理剖析》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

metabit

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值