惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Application and Cybersecurity Blog
Application and Cybersecurity Blog
A
About on SuperTechFans
S
SegmentFault 最新的问题
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Help Net Security
Help Net Security
有赞技术团队
有赞技术团队
博客园 - 【当耐特】
O
OpenAI News
美团技术团队
月光博客
月光博客
Apple Machine Learning Research
Apple Machine Learning Research
Schneier on Security
Schneier on Security
Webroot Blog
Webroot Blog
Cyberwarzone
Cyberwarzone
Hacker News - Newest:
Hacker News - Newest: "LLM"
Google Online Security Blog
Google Online Security Blog
T
Tenable Blog
S
Security Affairs
博客园_首页
S
Schneier on Security
Security Latest
Security Latest
T
Threat Research - Cisco Blogs
T
Tailwind CSS Blog
大猫的无限游戏
大猫的无限游戏
Spread Privacy
Spread Privacy
量子位
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
K
Kaspersky official blog
Hugging Face - Blog
Hugging Face - Blog
TaoSecurity Blog
TaoSecurity Blog
博客园 - 聂微东
Vercel News
Vercel News
M
MIT News - Artificial intelligence
T
Troy Hunt's Blog
B
Blog
MongoDB | Blog
MongoDB | Blog
Martin Fowler
Martin Fowler
Attack and Defense Labs
Attack and Defense Labs
L
LINUX DO - 最新话题
D
DataBreaches.Net
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Stack Overflow Blog
Stack Overflow Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园 - Franky
W
WeLiveSecurity
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
F
Fortinet All Blogs
www.infosecurity-magazine.com
www.infosecurity-magazine.com
C
Check Point Blog
H
Hacker News: Front Page

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes GC 设计与实现 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes EndPoint 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes 应用最佳实践 - 优雅关闭长连接 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 降本增效之应用优化 (一) Redis 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading Go 线程安全 map 方案选型 布隆过滤器 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading sync.Once Code Reading Go 无锁编程 sync/atomic Code Reading goroutine 交替打印奇偶数 GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 recover 必须在 defer 中调用? 为什么 defer 的执行顺序和注册顺序不同? Go map 设计与实现 Go 切片扩容底层实现 Go 语言中的零拷贝 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 缓存策略和模式 Go 内存模型 Kubernetes 核心概念
Go 数据竞态
2023-02-02 · via 蛮荆

2023-02-02 Golang 并发编程

概述

数据竞态 是并发系统编程中最常见和最难调试的错误类型之一。当两个 goroutine 同时访问同一个变量并且至少有一个是写入操作时, 就会发生 数据竞态。详细的信息,请参阅 Go 内存模型

使用方法

为了帮助诊断 数据竞态 导致的错误,Go 内置了一个 数据竞态检测器,只需要在具体的命令中加上 -race 参数即可。

$ go test -race mypkg    // 测试单个包
$ go run -race mysrc.go  // 运行单个文件
$ go build -race mycmd   // 构建命令
$ go install -race mypkg // 安装包

报告格式

数据竞态检测器 在程序中发现 数据竞态 时,会输出一份报告。该报告包含访问冲突的调用堆栈,以及相关 goroutine 的创建堆栈。

## 示例输出
WARNING: DATA RACE
Read by goroutine 185:
  net.(*pollServer).AddFD()
      src/net/fd_unix.go:89 +0x398
  net.(*pollServer).WaitWrite()
      src/net/fd_unix.go:247 +0x45
  net.(*netFD).Write()
      src/net/fd_unix.go:540 +0x4d4
  net.(*conn).Write()
      src/net/net.go:129 +0x101
  net.func·060()
      src/net/timeout_test.go:603 +0xaf
...
...

可选项

Go 的环境变量 GORACE 对应 数据竞态检测器 的参数设置,格式如下:

GORACE="option1=val1 option2=val2"

其中,options 参数的可选项为:

  • log_path (default stderr): 数据竞态检测器 将报告写入名为 log_path.pid 的文件中。stdoutstderr 则写入标准输出和标准错误
  • exitcode (default 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 休眠的毫秒数
## 示例
GORACE="log_path=/tmp/race/report strip_path_prefix=/my/go/sources/" go test -race

排除测试

当在命令中加入 -race 参数时,Go 定义了额外的构建标签 race,这样就可以使用标签在运行 数据竞态检测器 时排除一些代码和测试。 从本质上来说,其实也就是 条件编译

示例

有了 条件编译 之后,运行 go test -race 命令时,这个文件里面的单元测试就会被排除,不会运行。

// +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) {
	// ...
}

典型的数据竞争

下面是一些典型的数据竞争的小例子,所有的例子都可以使用 数据竞态检测器 检测到。

循环里面的数据竞争

package main

import (
	"fmt"
	"sync"
)

func main() {
	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()
		}()
	}
	wg.Wait()
}
$ go run -race main.go
## 输出如下 
5
5
5
==================
WARNING: DATA RACE
Read at 0x00c0000180f8 by goroutine 10:
  main.main.func1()
...
...
...
==================
5
5
Found 1 data race(s)
exit status 66

错误原因在于: 函数字面量中的 变量 i 与循环使用的变量相同,因此 goroutine 中的 读取与循环变量自增竞争,可以通过复制变量来修复该程序。

package main

import (
	"fmt"
	"sync"
)

func main() {
	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()
}
$ go run -race main.go
## 输出如下,你的输出可能和这里的不一样
4
1
0
2
3

不受保护的全局变量

如果从多个 goroutine 调用以下代码,则会导致 service map 的竞态。并发读写一个 map 是不安全的,解决方案可以使用 互斥锁

package main

import (
	"sync"
)

var service map[string]int

func RegisterService(name string, addr int) {
	// 并发写 map
	service[name] = addr
}

func LookupService(name string) int {
	return service[name]
}

func main() {
	service = make(map[string]int)

	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)

		go func(n int) {
			defer wg.Done()
			RegisterService("zero", n)
			LookupService("zero")
		}(i)
	}

	wg.Done()
}
$ go run -race main.go
## 输出如下
==================
WARNING: DATA RACE
Write at 0x00c00007e000 by goroutine 6:
  runtime.mapassign_faststr()
...
...
...
Found 2 data race(s)
exit status 66

解决方案: 通过增加 互斥锁 来消除 数据竞态,修正后的代码如下:

package main

import (
	"sync"
)

var (
	service   map[string]int
	serviceMu sync.Mutex
)

func RegisterService(name string, addr int) {
	// 并发写 map 之前,加锁
	serviceMu.Lock()
	defer serviceMu.Unlock()
	service[name] = addr
}

func LookupService(name string) int {
	serviceMu.Lock()
	defer serviceMu.Unlock()
	return service[name]
}

func main() {
	service = make(map[string]int)

	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)

		go func(n int) {
			defer wg.Done()
			RegisterService("zero", n)
			LookupService("zero")
		}(i)
	}

	wg.Done()
}
$ go run -race main.go
## 没有任何输出

从输出中可以看到,数据竞态 错误已经消除。

通道的发送和关闭操作未同步

同一个通道上面,发送和关闭操作如果不同步,也可能产生 数据竞态

package main

func main() {
	c := make(chan struct{})
	go func() { c <- struct{}{} }()
	close(c)
}
$ go run -race main.go
## 输出如下
==================
WARNING: DATA RACE
Read at 0x00c000110070 by goroutine 6:
  runtime.chansend()
...
...
...
Found 1 data race(s)
exit status 66

错误原因在于: 根据 Go 内存模型,通道上面的发送操作发生在该通道相应的接收完成之前

要同步发送和关闭操作,请使用 接收操作,可以保证发送操作在关闭之前完成

package main

func main() {
	c := make(chan struct{})
	go func() { c <- struct{}{} }()
	<-c
	close(c)
}
$ go run -race main.go
## 没有任何输出

从输出中可以看到,数据竞态 错误已经消除。

必要条件

数据竞态检测器 需要启用 cgo 并且支持 linux/amd64, linux/ppc64le, linux/arm64, freebsd/amd64, netbsd/amd64, darwin/amd64, darwin/arm64, and windows/amd64。

运行时开销

数据竞态 检测的成本因程序而异,一般来说,内存增加 5-10 倍,执行时间增加 2-20 倍

数据竞态检测器 为每个 deferrecover 语句额外分配 8 个字节,在 goroutine 退出之前,这些额外的分配不会被回收。 这意味着如果你有一个长期运行的 goroutine 定期调用 deferrecover,程序内存会无限增长。 这些内存分配不会显示在 runtime.ReadMemStatsruntime/pprof 的输出中。

通过上述官网描述的运行时开销,可以对我们的实践给予一定指导。比如: 生产环境中慎用 -race, 开发过程中单元测试使用 -race 多多益善。

扩展阅读