这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战。
前言
并发问题Race conditions 是隐晦容易出错的地方, 加上 go 中新建协程十分方便,只需要在方法前写个 go 就可以。 在代码部署到生产后很长时间才会发作。会导致一些比较难以琢磨的问题。
为了检测出可能存在的并发问题, Go 1.1 中引入了竞态检测器, 基于 C/C++ ThreadSanitizer 实现。已经在在 Go 中集成。
简介
Race conditions are among the most insidious and elusive programming errors. They typically cause erratic and mysterious failures, often long after the code has been deployed to production. While Go’s concurrency mechanisms make it easy to write clean concurrent code, they don’t prevent race conditions. Care, diligence, and testing are required. And tools can help.
使用示例
竞态检测与 Go 工具链完全集成,要启动竞态检测, 只需要加 -race 标志到命令行即可。
go test -race mypkg // test the package
go run -race mysrc.go // compile and run the program
go build -race mycmd // build the command
go install -race mypkg // install the package
测试代码:
代码简介: 使用定时器,0-1 秒随机时间间隔打印信息, 打印过程反复进行 5s, time.AfterFunc 创建第一条消息, 然后 Reset 方法调度下一条。 每次重复使用 一个 time.Timer 变量。
package main
import (
"fmt"
"math/rand"
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
start := time.Now()
var t *time.Timer
t = time.AfterFunc(randomDuration(), func() {
fmt.Println(time.Now().Sub(start))
t.Reset(randomDuration())
})
time.Sleep(5 * time.Second)
}
func randomDuration() time.Duration {
return time.Duration(rand.Int63n(1e9))
}
执行:
go run -race race.go
运行结果:
949.352745ms
==================
WARNING: DATA RACE
Read at 0x00c0000c0018 by goroutine 8:
main.main.func1()
/Users/xxxx/go/src/xxx.xxxx.org/xxxx.hit/GoProject/main/race/race.go:16 +0x126
Previous write at 0x00c0000c0018 by main goroutine:
main.main()
/Users/xxxx/go/src/code.xx.org/xxxx.hit/GoProject/main/race/race.go:14 +0x19e
Goroutine 8 (running) created at:
time.goFunc()
/usr/local/go/src/time/sleep.go:180 +0x51
==================
1.034796298s
1.702033798s
1.937836193s
2.225147392s
2.774472395s
3.408588095s
3.741477089s
3.92583422s
4.406456922s
Found 1 data race(s)
exit status 66
看看为啥会报错, go routines 对变量 t 有不同的读写操作。 如果定时器时间间隔非常小, 有可能出先一个什么问题?
var. t *time.Timer 此时是 nil,这样就有可能出现, 给 t 赋值的还没赋完, t.Reset 方法就执行了, 这个时候。t 是 nil, 这个时候就有问题了, 很可能会抛出一个错误 panic: runtime error: invalid memory address or nil pointer dereference
上面这段代码怎么改呢? 用 channel 改成阻塞的就可以
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
start := time.Now()
reset := make(chan bool) //channel
var t *time.Timer
t = time.AfterFunc(randomDuration(), func() {
fmt.Println(time.Now().Sub(start))
reset <- true
})
for time.Since(start) < 5*time.Second {
<-reset
t.Reset(randomDuration())
}
}
func randomDuration() time.Duration {
return time.Duration(rand.Int63n(1e9))
}
简单例子
package main
import (
"fmt"
"time"
)
func main() {
a := 1
go func() {
a = 2
}()
a = 3
fmt.Println("a is ", a)
time.Sleep(2 * time.Second)
}
上面代码中 主协程 会对 a 进行修改,里面的协程 go func 也会对 a 进行修改,会有问题。
原理
竞态检测器集成在 go 工具链中, 当设置了 -race 命令行标志时, 编译器将使用内存的时间和方式的代码记录下来,用于设置所有内存访问, 而运行时库会监视对共享变量的非同步访问。当检测到这个情况存在,会打出一个 WARING