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 测试
代码
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\n", 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()
}
执行三次查看结果,分别是
Final Counter: 399950
Final Counter: 399989
Final Counter: 400000
原因分析:每一次执行的时候,都使用 go routine(i) 启动了两个 goroutine,但是并没有控制它的执行顺序,并不能满足顺序一致性内存模型。
当然由于种种不确定性,所有肯定不止这两种情况,
2.1.2 data race 检测
上面问题的出现在上线后如果出现bug会非常难定位,因为不知道到底是哪里出现了问题,所以我们就要在测试阶段就结合 data race 工具提前发现问题。
- 使用
go run -race ./main.go
- 输出: 运行结果发现输出记录太长,调试的时候并不直观,结果如下
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"
可选配置见下表

- 配置
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
- 输出:
==================
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
- 说明:结果告诉可以看出 31 行这个地方有一个 goroutine 在读取数据,但是呢,在 33 行这个地方又有一个 goroutine 在写入,所以产生了数据竞争。
然后下面分别说明这两个 goroutine 是什么时候创建的,已经当前是否在运行当中。
2.2 循环中使用goroutine引用临时变量
代码如下:
func main() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i)
wg.Done()
}()
}
wg.Wait()
}
输出:常见的答案就是会输出 5 个 5,因为在 for 循环的 i++ 会执行的快一些,所以在最后打印的结果都是 5
这个答案不能说不对,因为真的执行的话大概率也是这个结果,但是不全。因为这里本质上是有数据竞争,在新启动的 goroutine 当中读取 i 的值,在 main 中写入,导致出现了 data race,这个结果应该是不可预知的,因为我们不能假定 goroutine 中 print 就一定比外面的 i++ 慢,习惯性的做这种假设在并发编程中是很有可能会出问题的正确示例:将 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 引起变量共享
代码
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
}
分析: 使用
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
修正: 在两个goroutine中使用新的临时变量
_, err := f1.Write(data)
...
_, err := f2.Write(data)
...
2.4 不受保护的全局变量
所谓全局变量是指,定义在多个函数的作用域之外,可以被多个函数或方法进行调用,常用的如 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]
}
要写出可测性比较高的代码就要少用或者是尽量避免用全局变量,使用 map 作为全局变量比较常见的一种情况就是配置信息。关于全局变量的话一般的做法就是加锁,或者也可以使用 sync.Ma
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 未受保护的成员变量
一般讲
成员变量指的是数据类型为结构体的某个字段。 如下一段代码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)
}
}
}()
}
使用原子操作
atomiictype 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 接口中存在的数据竞争
一个很有趣的例子 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\"\n", b.name)
} type Jerry struct {
name string
} func (j *Jerry) Hello() {
fmt.Printf("Jerry says, \"Hello my name is %s\"\n", 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()
}
}
这个例子有趣的点在于,最后输出的结果会有这种例子
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. 总结
- 使用
go build -race main.go和go test -race ./可以测试程序代码中是否存在数据竞争问题- 善用 data race 这个工具帮助我们提前发现并发错误
- 不要对未定义的行为做任何假设,虽然有时候我们写的只是一行代码,但是 go 编译器可能后面做了很多事情,并不是说一行写完就一定是原子的
- 即使是原子的出现了 data race 也不能保证安全,因为我们还有可见性的问题,上篇我们讲到了现代的 cpu 基本上都会有一些缓存的操作。
- 所有出现了 data race 的地方都需要进行处理
4 参考
- https://lailin.xyz/post/go-training-week3-data-race.html#典型案例
- https://dave.cheney.net/2014/06/27/ice-cream-makers-and-data-races
- http://blog.golang.org/race-detector
- https://golang.org/doc/articles/race_detector.html
- https://dave.cheney.net/2018/01/06/if-aligned-memory-writes-are-atomic-why-do-we-need-the-sync-atomic-package
3. Go并发编程--数据竞争的更多相关文章
- .NET并发编程-数据并行
本系列学习在.NET中的并发并行编程模式,实战技巧 内容目录 数据并行Fork/Join模式PLINQ 本小节开始学习数据并行的概念模式,以及在.NET中数据并行的实现方式.本系列保证最少代码呈现量, ...
- Java 多线程并发编程
导读 创作不易,禁止转载! 并发编程简介 发展历程 早起计算机,从头到尾执行一个程序,这样就严重造成资源的浪费.然后操作系统就出现了,计算机能运行多个程序,不同的程序在不同的单独的进程中运行,一个进程 ...
- python并发编程之多进程(三):共享数据&进程池
一,共享数据 展望未来,基于消息传递的并发编程是大势所趋 即便是使用线程,推荐做法也是将程序设计为大量独立的线程集合 通过消息队列交换数据.这样极大地减少了对使用锁定和其他同步手段的需求, 还可以扩展 ...
- c#中@标志的作用 C#通过序列化实现深表复制 细说并发编程-TPL 大数据量下DataTable To List效率对比 【转载】C#工具类:实现文件操作File的工具类 异步多线程 Async .net 多线程 Thread ThreadPool Task .Net 反射学习
c#中@标志的作用 参考微软官方文档-特殊字符@,地址 https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/toke ...
- python 使用多进程实现并发编程/使用queue进行进程间数据交换
import time import os import multiprocessing from multiprocessing import Queue, pool ""&qu ...
- [笔记][Java7并发编程实战手冊]3.8 并发任务间的数据交换Exchanger
[笔记][Java7并发编程实战手冊]系列文件夹 简单介绍 Exchanger 是一个同步辅助类.用于两个并发线程之间在一个同步点进行数据交换. 同意两个线程在某一个点进行数据交换. 本章exchan ...
- 《C#并发编程经典实例》学习笔记—3.1 数据的并行处理
问题 有一批数据,需要对每个元素进行相同的操作.该操作是计算密集型的,需要耗费一定的时间. 解决方案 常见的操作可以粗略分为 计算密集型操作 和 IO密集型操作.计算密集型操作主要是依赖于CPU计算, ...
- 伪共享(false sharing),并发编程无声的性能杀手
在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素.前段时间学习了一个牛X的高性能异步处理框架 Disruptor ...
- 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock
ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...
随机推荐
- CNN卷积神经网络详解
前言 在学计算机视觉的这段时间里整理了不少的笔记,想着就把这些笔记再重新整理出来,然后写成Blog和大家一起分享.目前的计划如下(以下网络全部使用Pytorch搭建): 专题一:计算机视觉基础 介 ...
- vue中v-show和v-if在显示和隐藏元素上的区别
v-show将元素隐藏是在dom节点上加style='display:none' v-if是直接将元素完全去掉 拿v-show示例,(v-if 也是一样,把下面的代码中v-show替换成v-if即可运 ...
- sql 中的with 语句使用
一直以来都很少使用sql中的with语句,但是看到了一篇文章中关于with的使用,它的确蛮好用,希望以后记得使用这个语句.一.with 的用法With alias_name as (select1)[ ...
- windows编译boost
1. https://www.boost.org 下载boost源码 boost_1_73_0.zip解压. 2.准备编译前的配置,打开vs2017 x86 CMD工具,进入目录boost_1_73_ ...
- servlet中servletContext的五大作用(四)
1. 获取web的上下文路径 2. 获取全局的参数 3. 作为域对象使用 4. 请求转发 5. 读取web项目的资源文件 package day10.about_serv ...
- 刷题-力扣-337. 打家劫舍 III
337. 打家劫舍 III 题目链接 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/house-robber-iii 著作权归领扣网络所有.商 ...
- 目录-理解ASP.NET Core
<理解ASP.NET Core>基于.NET5进行整理,旨在帮助大家能够对ASP.NET Core框架有一个清晰的认识. 目录 [01] Startup [02] Middleware [ ...
- Panel添加边框颜色和边框粗细调整
Panel控件添加边框颜色 C# WinForm窗体控件Panel修改边框颜色以及边框宽度方法 - JiYF - 博客园 (cnblogs.com) 1.新建一个用户控件的项目,如下: 2.添加一个P ...
- 查看局域网内所有的主机名、MAC地址和IP地址
查看所有 IP at MAC $ arp -a ? (10.125.49.187) at 18:81:e:eb:ef:c0 on en0 ifscope [ethernet] ? (10.125.50 ...
- Docker小白到实战之Dockerfile解析及实战演示,果然顺手
前言 使用第三方镜像肯定不是学习Docker的最终目的,最想要的还是自己构建镜像:将自己的程序.文件.环境等构建成自己想要的应用镜像,方便后续部署.启动和维护:而Dockerfile就是专门做这个事的 ...