- 前言
- Go 与操作系统
- Go 内部机制
- Go 基本数据类型
- 4 组合类型的使用
- 5 数据结构
- 6 Go package 中不为人知的知识
- 7 反射和接口
- 8 Go UNIX 系统编程
- 08.1 关于 UNIX 进程
- 08.2 flag 包
- 8.2 flag 包
- 08.3 io.Reader 和 io.Writer 接口
- 08.4 bufio 包
- 08.5 读取文本文件
- 08.6 从文件中读取所需的数据量
- 08.7 为什么我们使用二进制格式
- 08.8 读取 CSV 文件
- 08.9 写入文件
- 08.10 从磁盘加载和保存数据
- 08.11 再看strings包
- 08.12 关于bytes包
- 08.13 文件权限
- 08.14 处理 Unix 信号
- 08.15 Unix 管道编程
- 08.16 遍历目录树
- 08.17 使用 ePBF
- 08.18 关于 syscall.PtraceRegs
- 08.19 跟踪系统调用
- 08.20 User ID 和 group ID
- 08.21 其他资源
- 08.22 练习
- 08.23 总结
- 9 并发 Goroutines、Channel 和 Pipeline
- 10 Go 并发-进阶讨论
- 11 代码测试、优化及分析
- 12 Go 网络编程基础
- 13 网络编程 - 构建服务器与客户端
10.6 竞争状态
数据竞争状态 是两个多个运行中的如线程和 goroutines 试图控制或修改共享的资源或程序变量。严格讲,数据竞争发生在当两个或多个指令访问同一个内存地址,它们中至少有一个在此执行写操作。
当运行或构建 Go 源文件时使用 -race
标志会开启 Go 竞争监视器,它会使编译器创建一个典型的可执行文件的修改版。这个修改版可以记录所有对共享变量的访问以及发生的同步事件,包括调用 sync.Mutex
和 sync.WaitGroup
。分析相关事件后,竞争监视器打印一份报告来帮助您识别潜在问题,这样您就可以修正它们了。
请看下面的 Go 代码,它保存为 racec.go
。这个程序分三部分来介绍。
raceC.go
的第一部分如下:
package main
import (
"fmt"
"os"
"strconv"
"sync"
)
func main() {
arguments := os.Args
if len(arguments) != 2 {
fmt.Println("Give me a natural number!")
os.Exit(1)
}
numberGR, err := strconv.Atoi(os.Args[1])
if err != nil {
fmt.Println(err)
return
}
raceC.go
的第二段代码如下:
var waitGroup sync.WaitGroup
var i int
k := make(map[int]int)
k[1] = 12
for i = 0; i < numGR; i++ {
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
k[i] = i
}()
}
raceC.go
的其余代码如下:
k[2] = 10
waitGroup.Wait()
fmt.Println("k = %v\n", k)
}
好像许多 goroutines 同时访问 k
map 还不够,我们在调用 sync.Wait()
函数前再添加一个访问 k
map 的语句。
如果您执行 raceC.go
,会获得如下没有任何警告或错误信息的输出:
$go run raceC.go 10
k = map[int]int{7:10, 2:10, 10:10, 1:12}
$go run raceC.go 10
k = map[int]int{2:10, 10:10, 1:12, 8:8, 9:9}
$go run raceC.go 19
k = map[int]int{10:10, 1:12, 6:7, 7:7, 2:10}
如果您仅执行一次 raceC.go
,当打印 k
map 内容的时候尽管您没有得到期望的,但一切看起来正常。然而,多次执行 raceC.go
告诉我们这有些错误,主要是每次执行产生不同的输出。
如果我们决定使用 Go 竞争监视器分析 raceC.go
,我们可以获得更多信息及意外输出:
竞争监视器发现两处数据竞争。每个都在它的输出里用 WARNING: DATA RACE
消息开头。
第一个 数据竞争 发生在 main.main.func1()
内,它由一个goroutine 执行的 for
循环调用。这的问题由 Previous write
消息表示。检查相关代码后,很容易看到实际问题是匿名函数没带参数,意思是在 for
循环中使用的 i
值不同准确识别,因为这是个写操作,在 for
循环内不断改变。
第二处数据竞争信息是 Write at 0x00c420074180 by goroutine 7
。如果您阅读相关输出,会看到这个数据竞争是关于写操作的,并且至少有两个 goroutines 在执行。因为这两个 goroutine 有相同的名字(main.main.func1()
),这表明我们谈论的是同一个 goroutine。这两个 goroutine 试图写同一个变量,这就是数据竞争状态!
Go 用
main.main.func1()
记号命名一个内部地匿名函数。如果您有不同地匿名函数,它们的名字同样会不同
您可以问之际,现在为了修正这两个数居竞争引起的问题我能做什么?
好的,您可以重写 raceC.go
的 main()
函数如下:
func main() {
arguments := os.Args
if len(arguments) != 2 {
fmt.Println("Give me a natural number!")
os.Exit(1)
}
numGR, err := srconv.Atoi(os.Args[1])
if err != nil {
fmt.Println(err)
return
}
var waitGroup syncWaitGroup
var i int
k := make(map[int]int)
k[1] = 12
for i = 0; i < numGR; i++ {
waitGroup.Add(1)
go runc(j int) {
defer waitGroup.Done()
aMutex.Lock()
k[j] = j
aMutex.Unlock()
}(i)
}
waitGroup.Wait()
k[2] = 10
fmt.Printf("k = %#v\n", k)
}
aMutex
变量是一个定义在 main()
函数外的全局 sync.Mutex
变量,它可以在程序的任何地方访问到。尽管这不必要,不过有这样一个全局变量可以免得您总是把它传给函数。
把这个新版本 raceC.go
保存为 noRaceC.go
并执行它产生如下输出:
$go run noRaceC.go 10
k = map[int]int{1:1, 0:0, 5:5, 3:3, 6:6, 9:9, 2:10, 4:4, 7:7, 8:8}
用 Go 竞争监测器运行 noRaceC.go
产生如下输出:
$go run -race noRaceC.go 10
k = map[int]int{5:5, 7:7, 9:9, 1:1, 0:0, 4:4, 6:6, 8:8, 2:10, 3:3}
注意当访问 k
map 时,您需要一个锁机制。如果您没有使用这样的机制并且只修改了由 goroutine 执行的匿名函数的实现的话,您会从 go run noRaceC.go
获得如下输出:
这个根本问题是显而易见的:并发 map 写操作
。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论