Go并發(fā)編程實(shí)現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)
1.前言
雖然在 go 中,并發(fā)編程十分簡(jiǎn)單, 只需要使用 go func() 就能啟動(dòng)一個(gè) goroutine 去做一些事情,但是正是由于這種簡(jiǎn)單我們要十分當(dāng)心,不然很容易出現(xiàn)一些莫名其妙的 bug 或者是你的服務(wù)由于不知名的原因就重啟了。 而最常見的bug是關(guān)于線程安全方面的問題,比如對(duì)同一個(gè)map進(jìn)行寫操作。
2.數(shù)據(jù)競(jìng)爭(zhēng)
線程安全是否有什么辦法檢測(cè)到呢?
答案就是 data race tag,go 官方早在 1.1 版本就引入了數(shù)據(jù)競(jìng)爭(zhēng)的檢測(cè)工具,我們只需要在執(zhí)行測(cè)試或者是編譯的時(shí)候加上 -race 的 flag 就可以開啟數(shù)據(jù)競(jìng)爭(zhēng)的檢測(cè)
使用方式如下
go test -race main.go go build -race
不建議在生產(chǎn)環(huán)境 build 的時(shí)候開啟數(shù)據(jù)競(jìng)爭(zhēng)檢測(cè),因?yàn)檫@會(huì)帶來一定的性能損失(一般內(nèi)存5-10倍,執(zhí)行時(shí)間2-20倍),當(dāng)然 必須要 debug 的時(shí)候除外。
建議在執(zhí)行單元測(cè)試時(shí)始終開啟數(shù)據(jù)競(jìng)爭(zhēng)的檢測(cè)
2.1 示例一
執(zhí)行如下代碼,查看每次執(zhí)行的結(jié)果是否一樣
2.1.1 測(cè)試
代碼
package main import ( "fmt" "sync" ) var wg sync.WaitGroup var counter int func main() { // 多跑幾次來看結(jié)果 for i := 0; i < 100000; i++ { run() } fmt.Printf("Final Counter: %d\n", counter) } func run() { // 開啟兩個(gè) 協(xié)程,操作 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() }
執(zhí)行三次查看結(jié)果,分別是
Final Counter: 399950
Final Counter: 399989
Final Counter: 400000
原因分析:每一次執(zhí)行的時(shí)候,都使用 go routine(i) 啟動(dòng)了兩個(gè) goroutine,但是并沒有控制它的執(zhí)行順序,并不能滿足順序一致性內(nèi)存模型。
當(dāng)然由于種種不確定性,所有肯定不止這兩種情況,
2.1.2 data race 檢測(cè)
上面問題的出現(xiàn)在上線后如果出現(xiàn)bug會(huì)非常難定位,因?yàn)椴恢赖降资悄睦锍霈F(xiàn)了問題,所以我們就要在測(cè)試階段就結(jié)合 data race 工具提前發(fā)現(xiàn)問題。
使用
go run -race ./main.go
輸出: 運(yùn)行結(jié)果發(fā)現(xiàn)輸出記錄太長(zhǎng),調(diào)試的時(shí)候并不直觀,結(jié)果如下
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 配置
在官方的文檔當(dāng)中,可以通過設(shè)置 GORACE 環(huán)境變量,來控制 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
說明:結(jié)果告訴可以看出 31 行這個(gè)地方有一個(gè) goroutine 在讀取數(shù)據(jù),但是呢,在 33 行這個(gè)地方又有一個(gè) goroutine 在寫入,所以產(chǎn)生了數(shù)據(jù)競(jìng)爭(zhēng)。
然后下面分別說明這兩個(gè) goroutine 是什么時(shí)候創(chuàng)建的,已經(jīng)當(dāng)前是否在運(yùn)行當(dāng)中。
2.2 循環(huán)中使用goroutine引用臨時(shí)變量
代碼如下:
func main() { var wg sync.WaitGroup wg.Add(5) for i := 0; i < 5; i++ { go func() { fmt.Println(i) wg.Done() }() } wg.Wait() }
輸出:常見的答案就是會(huì)輸出 5 個(gè) 5,因?yàn)樵?for 循環(huán)的 i++ 會(huì)執(zhí)行的快一些,所以在最后打印的結(jié)果都是 5
這個(gè)答案不能說不對(duì),因?yàn)檎娴膱?zhí)行的話大概率也是這個(gè)結(jié)果,但是不全。因?yàn)檫@里本質(zhì)上是有數(shù)據(jù)競(jìng)爭(zhēng),在新啟動(dòng)的 goroutine 當(dāng)中讀取 i 的值,在 main 中寫入,導(dǎo)致出現(xiàn)了 data race,這個(gè)結(jié)果應(yīng)該是不可預(yù)知的,因?yàn)槲覀儾荒芗俣?goroutine 中 print 就一定比外面的 i++ 慢,習(xí)慣性的做這種假設(shè)在并發(fā)編程中是很有可能會(huì)出問題的
正確示例:將 i 作為參數(shù)傳入即可,這樣每個(gè) goroutine 拿到的都是拷貝后的數(shù)據(jù)
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) // 創(chuàng)建/寫入第一個(gè)文件 f1, err := os.Create("/tmp/file1") if err != nil { res <- err } else { go func() { // 下面的這個(gè)函數(shù)在執(zhí)行時(shí),是使用err進(jìn)行判斷,但是err的變量是個(gè)共享的變量 _, err = f1.Write(data) res <- err f1.Close() }() } // 創(chuàng)建寫入第二個(gè)文件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 執(zhí)行,可以發(fā)現(xiàn)這里報(bào)錯(cuò)的地方是,21 行和 28 行,有 data race,這里主要是因?yàn)楣蚕砹?err 這個(gè)變量
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
修正: 在兩個(gè)goroutine中使用新的臨時(shí)變量
_, err := f1.Write(data) ... _, err := f2.Write(data) ...
2.4 不受保護(hù)的全局變量
所謂全局變量是指,定義在多個(gè)函數(shù)的作用域之外,可以被多個(gè)函數(shù)或方法進(jìn)行調(diào)用,常用的如 map數(shù)據(jù)類型
// 定義一個(gè)全局變量 map數(shù)據(jù)類型 var service = map[string]string{} // RegisterService RegisterService // 用于寫入或更新key-value func RegisterService(name, addr string) { service[name] = addr } // LookupService LookupService // 用于查詢某個(gè)key-value func LookupService(name string) string { return service[name] }
要寫出可測(cè)性比較高的代碼就要少用或者是盡量避免用全局變量,使用 map 作為全局變量比較常見的一種情況就是配置信息。關(guān)于全局變量的話一般的做法就是加鎖,或者也可以使用 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 未受保護(hù)的成員變量
一般講成員變量 指的是數(shù)據(jù)類型為結(jié)構(gòu)體的某個(gè)字段。 如下一段代碼
type Watchdog struct{ last int64 } func (w *Watchdog) KeepAlive() { // 第一次進(jìn)行賦值操作 w.last = time.Now().UnixNano() } func (w *Watchdog) Start() { go func() { for { time.Sleep(time.Second) // 這里在進(jìn)行判斷的時(shí)候,很可能w.last更新正在進(jìn)行 if w.last < time.Now().Add(-10*time.Second).UnixNano() { fmt.Println("No keepalives for 10 seconds. Dying.") os.Exit(1) } } }() }
使用原子操作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 接口中存在的數(shù)據(jù)競(jìng)爭(zhēng)
一個(gè)很有趣的例子 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() } }
這個(gè)例子有趣的點(diǎn)在于,最后輸出的結(jié)果會(huì)有這種例子
Ben says, "Hello my name is Jerry"
Ben says, "Hello my name is Jerry"
這是因?yàn)槲覀冊(cè)趍aker = jerry這種賦值操作的時(shí)候并不是原子的,在上一篇文章中我們講到過,只有對(duì) single machine word 進(jìn)行賦值的時(shí)候才是原子的,雖然這個(gè)看上去只有一行,但是 interface 在 go 中其實(shí)是一個(gè)結(jié)構(gòu)體,它包含了 type 和 data 兩個(gè)部分,所以它的復(fù)制也不是原子的,會(huì)出現(xiàn)問題
type interface struct { Type uintptr // points to the type of the interface implementation Data uintptr // holds the data for the interface's receiver }
這個(gè)案例有趣的點(diǎn)還在于,這個(gè)案例的兩個(gè)結(jié)構(gòu)體的內(nèi)存布局一模一樣所以出現(xiàn)錯(cuò)誤也不會(huì) panic 退出,如果在里面再加入一個(gè) string 的字段,去讀取就會(huì)導(dǎo)致 panic,但是這也恰恰說明這個(gè)案例很可怕,這種錯(cuò)誤在線上實(shí)在太難發(fā)現(xiàn)了,而且很有可能會(huì)很致命。
3. 總結(jié)
使用 go build -race main.go和go test -race ./ 可以測(cè)試程序代碼中是否存在數(shù)據(jù)競(jìng)爭(zhēng)問題
- 善用 data race 這個(gè)工具幫助我們提前發(fā)現(xiàn)并發(fā)錯(cuò)誤
- 不要對(duì)未定義的行為做任何假設(shè),雖然有時(shí)候我們寫的只是一行代碼,但是 go 編譯器可能后面做了很多事情,并不是說一行寫完就一定是原子的
- 即使是原子的出現(xiàn)了 data race 也不能保證安全,因?yàn)槲覀冞€有可見性的問題,上篇我們講到了現(xiàn)代的 cpu 基本上都會(huì)有一些緩存的操作。
- 所有出現(xiàn)了 data race 的地方都需要進(jìn)行處理
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
到此這篇關(guān)于Go并發(fā)編程實(shí)現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)的文章就介紹到這了,更多相關(guān)Go 數(shù)據(jù)競(jìng)爭(zhēng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
GO語(yǔ)言實(shí)現(xiàn)二維碼掃碼的示例代碼
你對(duì)二維碼掃碼的流程有困惑嗎,這篇文章就結(jié)合筆者自身的開發(fā)經(jīng)驗(yàn)進(jìn)行分享,讓大家熟悉并掌握此功能,感興趣的小伙伴快跟隨小編一起學(xué)習(xí)一下吧2023-06-06Golang實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)Stack(堆棧)的示例詳解
在計(jì)算機(jī)科學(xué)中,stack(棧)是一種基本的數(shù)據(jù)結(jié)構(gòu),它是一種線性結(jié)構(gòu),具有后進(jìn)先出(Last In First Out)的特點(diǎn)。本文將通過Golang實(shí)現(xiàn)堆棧,需要的可以參考一下2023-04-04Golang 獲取文件md5校驗(yàn)的方法以及效率對(duì)比
這篇文章主要介紹了Golang 獲取文件md5校驗(yàn)的方法以及效率對(duì)比,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-05-05Go語(yǔ)言實(shí)戰(zhàn)之實(shí)現(xiàn)一個(gè)簡(jiǎn)單分布式系統(tǒng)
如今很多云原生系統(tǒng)、分布式系統(tǒng),例如?Kubernetes,都是用?Go?語(yǔ)言寫的,這是因?yàn)?Go?語(yǔ)言天然支持異步編程。本篇文章將介紹如何用?Go?語(yǔ)言編寫一個(gè)簡(jiǎn)單的分布式系統(tǒng),需要的小伙伴開業(yè)跟隨小編一起學(xué)習(xí)一下2022-10-10淺析Go語(yǔ)言中數(shù)組的這些細(xì)節(jié)
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言中數(shù)組一些細(xì)節(jié)的相關(guān)資料,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Go語(yǔ)言有一定的幫助,需要的可以了解一下2022-11-11