介绍

共享数据竞争问题是并发系统中常见且难排查的问题.

什么是数据竞争?

当两个协程goroutine同时访问相同的共享变量,其中有一个执行了写操作,或两个都执行了写操作,就会出现数据竞争问题,导致数据异常.详情请参考Go内存模型详解:https://golang.org/ref/mem

如以下代码由于并发访问同一个map,存在数据冲突,一定概率导致数据异常,程序崩溃:

package mainimport "fmt"func main(){   c := make(chan bool)   m := make(map[string]string)   go func(){      m["1"] = "a"  //运行时,这里有个协程对m进行写操作      c 

多次执行后出现,程序崩溃,提示map并发写错误:

怎么使用数据冲突检测器?

在go中,已经内置数据冲突检测器,直接在go命令行添加参数-race即可,如以下4种方式:

  • go test -race mypkg //测试mypkg包
  • go run -race main.go //带冲突检测调试运行源代码main.go
  • go build -race main.go //带冲突检测编译源代码
  • go install -race mypkg//安装mypkg包

冲突报告格式

检测到冲突时,会按照以下格式打印冲突报告,它包含堆栈跟踪信息,以及协程编号,如:

➜  dataRace git:(master) ✗ go run -race main.go==================WARNING: DATA RACE  //警告: 数据冲突Write at 0x00c000124180 by goroutine 7:  //普通协程写操作  runtime.mapassign_faststr()      /usr/local/go/src/runtime/map_faststr.go:202 +0x0  main.main.func1()      /Users/xb/gitlab/go/go_core_program/sync/dataRace/main.go:9 +0x5dPrevious write at 0x00c000124180 by main goroutine:  //主协程写操作  runtime.mapassign_faststr()      /usr/local/go/src/runtime/map_faststr.go:202 +0x0  main.main()      /Users/xb/gitlab/go/go_core_program/sync/dataRace/main.go:13 +0xcbGoroutine 7 (running) created at:  main.main()      /Users/xb/gitlab/go/go_core_program/sync/dataRace/main.go:8 +0x9c==================2 b1 aFound 1 data race(s)  //找到一处数据冲突exit status 66

运行选项

GORACE环境变量用于设置数据冲突检测选项, 在执行go程序前设置该值即生效,格式如下:

GORACE="选项1=值1 选项2=值2 ..."

包含以下常用选项:

  • log_path (default stderr): 日志文件前缀,冲突检测结果存入log_path.pid. 特别的,如果配置为stdout则输出到标准输出,配置为stderr则输出到错误输出
  • exitcode (default 66): 检测到冲突时,程序的退出状态码,可以自定义,默认为66
  • strip_path_prefix (default ""): 为了使报告简洁,该选项用于去除文件路径中这些前缀
  • history_size (default 1): 每个协程goroutine内存访问历史大小为32K*2**history_size 个元素,增加该选项值可以避免在报告中出现"恢复堆栈失败错误",但是会增加内存开销
  • halt_on_error (default 0): 用于控制程序遇到数据竞争时,是否退出,默认不会退出,只打印错误信息.
  • atexit_sleep_ms (default 1000): 在主线程goroutine中等待多少毫秒后退出,默认1秒

排除单元测试

当你使用-race参数构建时,go命令定义了更多的构建标记,当你运行冲突检测时,你可以使用这些标记排除代码和单元测试,比如:

// +build !race   构建约束,用于排除有冲突的测试package foo// The test contains a data race. See issue 123. func TestFoo(t *testing.T) {// ... 包含数据冲突}// The test fails under the race detector due to timeouts.func TestBar(t *testing.T) {// ... 包含冲突检测超时导致失败的代码}// The test takes too long under the race detector. func TestBaz(t *testing.T) {// ...  包含在冲突检测下执行耗时太长的代码}

使用冲突检测时注意事项

当使用go test -race做冲突检测时,检测器只会检测运行时的冲突,没有执行的代码块不会进行检测,如果你的单元测试是不完全覆盖,你需要使用go build -race构建一个完整的二进制包进行检测

典型数据冲突场景

  • Race on loop counter 循环计数器冲突
package mainimport (   "fmt"   "sync")func main() {   //以下代码由于并发,同时获取值,存在冲突,所以i不会按照预期(012345)打印,比如打印55555,   var wg sync.WaitGroup   wg.Add(5)   for i := 0; i < 5; i++ {      go func() {         fmt.Println(i) // Not the 'i' you are looking for.         wg.Done()      }()   }}

解决办法:对变量拷贝一份出来,新的变量指向不同的内存地址

package mainimport (   "fmt"   "sync")func main() {   //读取本地拷贝值j,与i指向不同的地址,解决冲突   var wg sync.WaitGroup   wg.Add(5)   for i := 0; i < 5; i++ {      go func(j int) {         fmt.Println(j) // Good. Read local copy of the loop counter.         wg.Done()      }(i)   }   wg.Wait()}
  • 由于意外,共享了变量导致冲突
func ParallelWrite(data []byte) chan error {   res := make(chan error, 2)   f1, err := os.Create("file1")   if err != nil {      res 

解决办法: 重新分配err变量

..._, err := f1.Write(data)..._, err := f2.Write(data)...
  • 未加保护的全局变量
var service map[string]net.Addrfunc RegisterService(name string, addr net.Addr) {   service[name] = addr}func LookupService(name string) net.Addr {   return service[name]}

以上代码中,map在多个协程中并发中读写会导致冲突

解决方法: 使用互斥锁,保证同时只能读或者写

var (   service   map[string]net.Addr   serviceMu sync.Mutex)func RegisterService(name string, addr net.Addr) {   serviceMu.Lock()   defer serviceMu.Unlock()   service[name] = addr}func LookupService(name string) net.Addr {   serviceMu.Lock()   defer serviceMu.Unlock()   return service[name]}
  • 使用了不受保护的基本数据类型

基本数据类型,如bool, int, int64等也存在数据冲突,这种问题难以排查,一般都是由于非原子的内存访问引起的,如:

type Watchdog struct{ last int64 }func (w *Watchdog) KeepAlive() {   w.last = time.Now().UnixNano() // First conflicting access. 写操作,与下面的读操作构成冲突}func (w *Watchdog) Start() {   go func() {      for {         time.Sleep(time.Second)         // Second conflicting access. 这里是读操作         if w.last < time.Now().Add(-10*time.Second).UnixNano() {            fmt.Println("No keepalives for 10 seconds. Dying.")            os.Exit(1)         }      }   }()}

解决方法:使用通道或者互斥锁mutex, 也可以使用无锁的sync/atomic包,如:

type Watchdog struct{ last int64 }func (w *Watchdog) KeepAlive() {   atomic.StoreInt64(&w.last, time.Now().UnixNano()) //使用原子包存储方法}func (w *Watchdog) Start() {   go func() {      for {         time.Sleep(time.Second)         if atomic.LoadInt64(&w.last) < time.Now().Add(-10*time.Second).UnixNano() {  //使用原子包的读取方法            fmt.Println("No keepalives for 10 seconds. Dying.")            os.Exit(1)         }      }   }()}
  • 未同步的发送和关闭操作,如:
c := make(chan struct{}) // or buffered channel 这里也可以使用带缓冲的通道演示// The race detector cannot derive the happens before relation// for the following send and close operations. These two operations 下面的通道发送和关闭操作没有进行同步,导致冲突// are unsynchronized and happen concurrently.go func() { c 

解决方法:通道关闭前,增加一个读取操作,完成同步

c := make(chan struct{}) // or buffered channelgo func() { c 
  • 单例模式场景也使用锁避免冲突
package mainimport (   "fmt"   "sync"   "sync/atomic")//定义单例结构体type singleton struct {}var(   instance *singleton   initialized uint32  //初始化标志,用于标识是否已经被初始化   mu sync.Mutex  //互斥锁对象)func Instance() *singleton{   if atomic.LoadUint32(&initialized)==1{  //如果实例已经初始化,直接返回      return instance   }   //如果没有实例化,则用锁同步执行下面的代码,即同一时间只能有一个协程进入执行以下代码块   mu.Lock()   defer mu.Unlock()   if instance==nil{   defer atomic.StoreUint32(&initialized, 1)   instance = &singleton{}   }   return instance}func main(){   mySingleton := Instance()   fmt.Printf("单例模式得到的对象:%v", mySingleton)}
  • 与单例模式类似的,用锁实现某个方法只调用一次(DoOnce)
package mainimport (   "sync"   "sync/atomic")type Once struct{   m sync.Mutex   done uint32}//传入一个回调函数,保证只执行一次该回调函数func(o *Once)Do(f func()){   if atomic.LoadUint32(&o.done) == 1 {      return   }   o.m.Lock()   defer o.m.Unlock()   if o.done == 0 {      defer atomic.StoreUint32(&o.done, 1)      f()  //回调函数   }}

数据冲突检测器当前支持的系统

  • linux/amd64
  • linux/ppc64le
  • linux/arm64
  • freebsd/amd64
  • netbsd/amd64
  • darwin/amd64
  • windows/amd64

运行时开销

开启冲突检测,通常程序的内存使用会增加5~10倍,执行耗时增加2~20倍.

注意事项: 数据冲突检测器为每个defer和recover语句分配额外8字节,该内存直到协程退出才会释放,这意味着如果你有一个长时间运行的协程goroutine,它会周期性的调用defer和recover,导致程序内存使用持续增长,且这些内存分配不会显示在runtime.ReadMemStats(运行时读内存统计)和runtime/pprof(运行时性能调试工具pprof统计)中.

参考文档:

https://golang.org/doc/articles/race_detector.html

Go语言高级编程 (Advanced Go Programming)

golang int64转string_Golang 并发数据冲突检测器与并发安全相关推荐

  1. 【Golang 快速入门】高级语法:反射 + 并发

    Golang 快速入门 Golang 进阶 反射 变量内置 Pair 结构 reflect 结构体标签 并发知识 基础知识 早期调度器的处理 GMP 模型 调度器的设计策略 并发编程 goroutin ...

  2. golang int64转int

    背景: 在cgo中,需要将golang的整数类型传到cgo中使用,但是不能直接传golang的int类型,而应该是 int32, 数值的内存长度才能与 c中int 长度一致. 但是有时 golang中 ...

  3. Java 并发/多线程教程(四)-并发模型

    本系列译自jakob jenkov的Java并发多线程教程(本章节部分内容参考http://ifeve.com/并发编程模型),个人觉得很有收获.由于个人水平有限,不对之处还望矫正! 并发系统可以有多 ...

  4. 高并发编程_高并发编程系列:7大并发容器详解(附面试题和企业编程指南)...

    不知道从什么时候起,在Java编程中,经常听到Java集合类,同步容器.并发容器,高并发编程成为当下程序员需要去了解掌握的技术之一,那么他们有哪些具体分类,以及各自之间的区别和优劣呢? 只有把这些梳理 ...

  5. 《Java并发编程入门与高并发面试》or 《Java并发编程与高并发解决方案》笔记

    <Java并发编程入门与高并发面试>or <Java并发编程与高并发解决方案>笔记 参考文章: (1)<Java并发编程入门与高并发面试>or <Java并发 ...

  6. C语言并发执行的进程怎么写,多进程并发写文件 多进程并发售票 用c语言写

    tcp多进程并发文件服务器代码? 线程是相对独立的执行单位,是计算机系统进行调度的最小单位,其切换由操作系统控制,称之为短作业调度.换句话说您没有任何必要去手动调度线程.如果您想要实现的是连接分配的话 ...

  7. Java 高并发_JAVA并发编程与高并发解决方案 JAVA高并发项目实战课程 没有项目经验的朋友不要错过!...

    JAVA并发编程与高并发解决方案 JAVA高并发项目实战课程 没有项目经验的朋友不要错过! 1.JPG (37.82 KB, 下载次数: 0) 2018-12-3 09:40 上传 2.JPG (28 ...

  8. 《Java 高并发》01 高并发基本概念

    基本概念 同步和异步 同步和异步通常是用来形容一次方法调用. 同步方法调用一旦开始,调用者必须等到方法返回才能继续执行后续操作. 异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就 ...

  9. 都在讨论高并发,结果连并发量、TPS、QPS都分不清

    " 年年岁岁跳槽季,回回必问高并发!原因很简单,因为高并发能牵扯出太多问题,接口响应超时.CPU负载升高.GC频繁.死锁.大数据量存储等,能考察求职者的真实情况. 而很多人在第一步就倒下了! ...

最新文章

  1. 如何查看 phtml文件 并有代码颜色提示
  2. nagios+cacit 整合(rpm)
  3. android背景不填充,(Android Studio)应用程序背景图像不填充屏幕
  4. 利用c语言建立交易系统,【图】手把手教会你构建自己的交易系统 - 4_股票论坛,炒股公式,股票指标,股票公式,选股公式_数据、教程交流论坛_理想论坛 - 股票论坛...
  5. java 文件追加文本_Java追加文件内容的三种方法
  6. Apache AB 性能测试
  7. oracle apex表隐藏,oracle-apex 处理表格形式
  8. java 1.8 内存告警问题
  9. 将页面强制保持在所属框架中
  10. 浅谈网站渗透的常用方法和一般思路
  11. linux cpu使用率500%,Linux:CPU使用率100%排查方法
  12. Awesome Competitive Programming
  13. c语言编程实现简单三子棋游戏
  14. Diffusion Model
  15. 护卫神mysql初始密码_护卫神MySQL密码重置器(MySQL密码重置工具)V1.3 最新免费版...
  16. int bool str (索引,切片) for 循环
  17. 趣题:奇怪的自然数集划分
  18. 2019/7/2 图书馆检索·真·简陋版(新人学习向)
  19. Java实现elastic中服务接口性能指标统计(接口QPS、接口99响应时间等)并存入表
  20. spring2.5.6整合hibernate3.3.2_Annotation声明式事务管理-第二节

热门文章

  1. linux下yum错误:[Errno 14] problem making ssl connection Trying other mirror.
  2. LeetCode简单题之检查是否所有 A 都在 B 之前
  3. 固态硬盘与QLC闪存
  4. GStreamer 1.18.4稳定的错误修复版本
  5. 2021年大数据Hive(七):Hive的开窗函数
  6. Android Fragment 调用宿主Activity 里面的方法
  7. pfSense 2.4.3 发布,包含重要的安全修复补丁
  8. VisualStudio 合并代码文件
  9. 机器学习入门(17)— 输入 4 维数据、基于 im2col 展开来实现卷积层
  10. appium-DesiredCapability详解与实战