• 3. Go并发编程--数据竞争


    1.前言

    虽然在 go 中,并发编程十分简单, 只需要使用 go func() 就能启动一个 goroutine 去做一些事情,但是正是由于这种简单我们要十分当心,不然很容易出现一些莫名其妙的 bug 或者是你的服务由于不知名的原因就重启了。 而最常见的bug是关于线程安全方面的问题,比如对同一个map进行写操作。

    2.数据竞争

    线程安全是否有什么办法检测到呢?

    答案就是 data race tag,go 官方早在 1.1 版本就引入了数据竞争的检测工具,我们只需要在执行测试或者是编译的时候加上 -race 的 flag 就可以开启数据竞争的检测

    使用方式如下

    go test -race main.go
    go build -race
    

    不建议在生产环境 build 的时候开启数据竞争检测,因为这会带来一定的性能损失(一般内存5-10倍,执行时间2-20倍),当然 必须要 debug 的时候除外。
    建议在执行单元测试时始终开启数据竞争的检测

    2.1 示例一

    执行如下代码,查看每次执行的结果是否一样

    2.1.1 测试

    1. 代码

      package main
      
      import (
      	"fmt"
      	"sync"
      )
      
      var wg sync.WaitGroup
      var counter int
      
      func main() {
      	// 多跑几次来看结果
      	for i := 0; i < 100000; i++ {
      		run()
      	}
      	fmt.Printf("Final Counter: %d
      ", counter)
      }
      
      
      func run() {
          // 开启两个 协程,操作
      	for i := 1; i <= 2; i++ {
      		wg.Add(1)
      		go routine(i)
      	}
      	wg.Wait()
      }
      
      func routine(id int) {
      	for i := 0; i < 2; i++ {
      		value := counter
      		value++
      		counter = value
      	}
      	wg.Done()
      }
      
    2. 执行三次查看结果,分别是

      Final Counter: 399950
      Final Counter: 399989
      Final Counter: 400000
      
    3. 原因分析:每一次执行的时候,都使用 go routine(i) 启动了两个 goroutine,但是并没有控制它的执行顺序,并不能满足顺序一致性内存模型。

      当然由于种种不确定性,所有肯定不止这两种情况,

    2.1.2 data race 检测

    上面问题的出现在上线后如果出现bug会非常难定位,因为不知道到底是哪里出现了问题,所以我们就要在测试阶段就结合 data race 工具提前发现问题。

    1. 使用
      go run -race ./main.go
      
    2. 输出: 运行结果发现输出记录太长,调试的时候并不直观,结果如下
      main.main()
            D:/gopath/src/Go_base/daily_test/data_race/demo.go:14 +0x44
      ==================
      Final Counter: 399987
      Found 1 data race(s)
      exit status 66
      

    2.1.3 data race 配置

    在官方的文档当中,可以通过设置 GORACE 环境变量,来控制 data race 的行为, 格式如下:

    
    GORACE="option1=val1 option2=val2"
    

    可选配置见下表

    1. 配置
      GORACE="halt_on_error=1 strip_path_prefix=/mnt/d/gopath/src/Go_base/daily_test/data_race/01_data_race" go run -race ./demo.go
      
    2. 输出:
      ==================
      WARNING: DATA RACE
      Read at 0x00000064d9c0 by goroutine 8:
        main.routine()
            /mnt/d/gopath/src/Go_base/daily_test/data_race/demo.go:31 +0x47
      
      Previous write at 0x00000064d9c0 by goroutine 7:
        main.routine()
            /mnt/d/gopath/src/Go_base/daily_test/data_race/demo.go:33 +0x64
      
      Goroutine 8 (running) created at:
        main.run()
            /mnt/d/gopath/src/Go_base/daily_test/data_race/demo.go:24 +0x75
        main.main()
            /mnt/d/gopath/src/Go_base/daily_test/data_race/demo.go:14 +0x3c
      
      Goroutine 7 (finished) created at:
        main.run()
            /mnt/d/gopath/src/Go_base/daily_test/data_race/demo.go:24 +0x75
        main.main()
            /mnt/d/gopath/src/Go_base/daily_test/data_race/demo.go:14 +0x3c
      ==================
      exit status 66
      
    3. 说明:结果告诉可以看出 31 行这个地方有一个 goroutine 在读取数据,但是呢,在 33 行这个地方又有一个 goroutine 在写入,所以产生了数据竞争。
      然后下面分别说明这两个 goroutine 是什么时候创建的,已经当前是否在运行当中。

    2.2 循环中使用goroutine引用临时变量

    1. 代码如下:

      func main() {
      	var wg sync.WaitGroup
      	wg.Add(5)
      	for i := 0; i < 5; i++ {
      		go func() {
      			fmt.Println(i) 
      			wg.Done()
      		}()
      	}
          wg.Wait()
      }
      
    2. 输出:常见的答案就是会输出 5 个 5,因为在 for 循环的 i++ 会执行的快一些,所以在最后打印的结果都是 5
      这个答案不能说不对,因为真的执行的话大概率也是这个结果,但是不全。因为这里本质上是有数据竞争,在新启动的 goroutine 当中读取 i 的值,在 main 中写入,导致出现了 data race,这个结果应该是不可预知的,因为我们不能假定 goroutine 中 print 就一定比外面的 i++ 慢,习惯性的做这种假设在并发编程中是很有可能会出问题的

    3. 正确示例:将 i 作为参数传入即可,这样每个 goroutine 拿到的都是拷贝后的数据

      func main() {
      	var wg sync.WaitGroup
      	wg.Add(5)
      	for i := 0; i < 5; i++ {
      		go func(i int) {
      			fmt.Println(i)
      			wg.Done()
      		}(i)
      	}
      	wg.Wait()
      }
      

    2.3 引起变量共享

    1. 代码

      package main
      
      import "os"
      
      func main() {
      	ParallelWrite([]byte("xxx"))
      }
      
      // ParallelWrite writes data to file1 and file2, returns the errors.
      func ParallelWrite(data []byte) chan error {
      	res := make(chan error, 2)
      	
      	// 创建/写入第一个文件
      	f1, err := os.Create("/tmp/file1")
      	
      	if err != nil {
      		res <- err
      	} else {
      		go func() {
      			// 下面的这个函数在执行时,是使用err进行判断,但是err的变量是个共享的变量
      			_, err = f1.Write(data)
      			res <- err
      			f1.Close()
      		}()
      	}
      	
      	 // 创建写入第二个文件n
      	f2, err := os.Create("/tmp/file2")
      	if err != nil {
      		res <- err
      	} else {
      		go func() {
      			_, err = f2.Write(data)
      			res <- err
      			f2.Close()
      		}()
      	}
      	return res
      }
      
    2. 分析: 使用 go run -race main.go 执行,可以发现这里报错的地方是,21 行和 28 行,有 data race,这里主要是因为共享了 err 这个变量

      root@failymao:/mnt/d/gopath/src/Go_base/daily_test/data_race# go run -race demo2.go
      ==================
      WARNING: DATA RACE
      Write at 0x00c0001121a0 by main goroutine:
        main.ParallelWrite()
            /mnt/d/gopath/src/Go_base/daily_test/data_race/demo2.go:28 +0x1dd
        main.main()
            /mnt/d/gopath/src/Go_base/daily_test/data_race/demo2.go:6 +0x84
      
      Previous write at 0x00c0001121a0 by goroutine 7:
        main.ParallelWrite.func1()
            /mnt/d/gopath/src/Go_base/daily_test/data_race/demo2.go:21 +0x94
      
      Goroutine 7 (finished) created at:
        main.ParallelWrite()
            /mnt/d/gopath/src/Go_base/daily_test/data_race/demo2.go:19 +0x336
        main.main()
            /mnt/d/gopath/src/Go_base/daily_test/data_race/demo2.go:6 +0x84
      ==================
      Found 1 data race(s)
      exit status 66
      
    3. 修正: 在两个goroutine中使用新的临时变量

      _, err := f1.Write(data)
      ...
      _, err := f2.Write(data)
      ...
      

    2.4 不受保护的全局变量

    1. 所谓全局变量是指,定义在多个函数的作用域之外,可以被多个函数或方法进行调用,常用的如 map数据类型

      // 定义一个全局变量 map数据类型
      var service = map[string]string{}
      
      // RegisterService RegisterService
      // 用于写入或更新key-value
      func RegisterService(name, addr string) {
      	service[name] = addr
      }
      
      // LookupService LookupService
      // 用于查询某个key-value
      func LookupService(name string) string {
      	return service[name]
      }
      
    2. 要写出可测性比较高的代码就要少用或者是尽量避免用全局变量,使用 map 作为全局变量比较常见的一种情况就是配置信息。关于全局变量的话一般的做法就是加锁,或者也可以使用 sync.Map

      var (
      service   map[string]string
      serviceMu sync.Mutex
      )
      
      func RegisterService(name, addr string) {
      	serviceMu.Lock()
      	defer serviceMu.Unlock()
      	service[name] = addr
      }
      
      func LookupService(name string) string {
      	serviceMu.Lock()
      	defer serviceMu.Unlock()
      	return service[name]
      }
      

    2.5 未受保护的成员变量

    1. 一般讲成员变量 指的是数据类型为结构体的某个字段。 如下一段代码

      type Watchdog struct{ 
          last int64
      }
      
      func (w *Watchdog) KeepAlive() {
          // 第一次进行赋值操作
      	w.last = time.Now().UnixNano() 
      }
      
      func (w *Watchdog) Start() {
      	go func() {
      		for {
      			time.Sleep(time.Second)
      			// 这里在进行判断的时候,很可能w.last更新正在进行
      			if w.last < time.Now().Add(-10*time.Second).UnixNano() {
      				fmt.Println("No keepalives for 10 seconds. Dying.")
      				os.Exit(1)
      			}
      		}
      	}()
      }
      
    2. 使用原子操作atomiic

      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)
      			}
      		}
      	}()
      }
      

    2.6 接口中存在的数据竞争

    1. 一个很有趣的例子 Ice cream makers and data races

      package main
      
      import "fmt"
      
      type IceCreamMaker interface {
      	// Great a customer.
      	Hello()
      }
      
      type Ben struct {
      	name string
      }
      
      func (b *Ben) Hello() {
      	fmt.Printf("Ben says, "Hello my name is %s"
      ", b.name)
      }
      
      type Jerry struct {
      	name string
      }
      
      func (j *Jerry) Hello() {
      	fmt.Printf("Jerry says, "Hello my name is %s"
      ", j.name)
      }
      
      func main() {
      	var ben = &Ben{name: "Ben"}
      	var jerry = &Jerry{"Jerry"}
      	var maker IceCreamMaker = ben
      
      	var loop0, loop1 func()
      
      	loop0 = func() {
      		maker = ben
      		go loop1()
      	}
      
      	loop1 = func() {
      		maker = jerry
      		go loop0()
      	}
      
      	go loop0()
      
      	for {
      		maker.Hello()
      	}
      }
      
    2. 这个例子有趣的点在于,最后输出的结果会有这种例子

      Ben says, "Hello my name is Jerry"
      Ben says, "Hello my name is Jerry"
      

      这是因为我们在maker = jerry这种赋值操作的时候并不是原子的,在上一篇文章中我们讲到过,只有对 single machine word 进行赋值的时候才是原子的,虽然这个看上去只有一行,但是 interface 在 go 中其实是一个结构体,它包含了 type 和 data 两个部分,所以它的复制也不是原子的,会出现问题

      type interface struct {
         Type uintptr     // points to the type of the interface implementation
         Data uintptr     // holds the data for the interface's receiver
      }
      

      这个案例有趣的点还在于,这个案例的两个结构体的内存布局一模一样所以出现错误也不会 panic 退出,如果在里面再加入一个 string 的字段,去读取就会导致 panic,但是这也恰恰说明这个案例很可怕,这种错误在线上实在太难发现了,而且很有可能会很致命。

    3. 总结

    1. 使用 go build -race main.gogo test -race ./ 可以测试程序代码中是否存在数据竞争问题
      • 善用 data race 这个工具帮助我们提前发现并发错误
      • 不要对未定义的行为做任何假设,虽然有时候我们写的只是一行代码,但是 go 编译器可能后面做了很多事情,并不是说一行写完就一定是原子的
      • 即使是原子的出现了 data race 也不能保证安全,因为我们还有可见性的问题,上篇我们讲到了现代的 cpu 基本上都会有一些缓存的操作。
      • 所有出现了 data race 的地方都需要进行处理

    4 参考

    1. https://lailin.xyz/post/go-training-week3-data-race.html#典型案例
    2. https://dave.cheney.net/2014/06/27/ice-cream-makers-and-data-races
    3. http://blog.golang.org/race-detector
    4. https://golang.org/doc/articles/race_detector.html
    5. https://dave.cheney.net/2018/01/06/if-aligned-memory-writes-are-atomic-why-do-we-need-the-sync-atomic-package
    ♥永远年轻,永远热泪盈眶♥
  • 相关阅读:
    服务方式加载卸载NT驱动函数集
    《Windows核心编程》学习笔记(12)– 虚拟内存
    《Windows核心编程》学习笔记(14)– 堆
    数据库连接错误:提示TCP端口1433,sql server 2008 Connection refused:connect
    Windows驱动开发技术详解笔记
    Struts2文件上传的大小限制问题
    pragma comment的使用 pragma预处理指令详解
    解决FastCGI Error Error Number: 2147467259 (0x80004005). 和 Error Number: 1073741819 (0xc0000005).
    PHP中的日期处理
    mysql远程连接10061错误
  • 原文地址:https://www.cnblogs.com/failymao/p/15336368.html
Copyright © 2020-2023  润新知