golang int64转string_Golang 并发数据冲突检测器与并发安全
介紹
共享數據競爭問題是并發系統中常見且難排查的問題.
什么是數據競爭?
當兩個協程goroutine同時訪問相同的共享變量,其中有一個執行了寫操作,或兩個都執行了寫操作,就會出現數據競爭問題,導致數據異常.詳情請參考Go內存模型詳解:https://golang.org/ref/mem
如以下代碼由于并發訪問同一個map,存在數據沖突,一定概率導致數據異常,程序崩潰:
package mainimport "fmt"func main(){ c := make(chan bool) m := make(map[string]string) go func(){ m["1"] = "a" //運行時,這里有個協程對m進行寫操作 c 多次執行后出現,程序崩潰,提示map并發寫錯誤:
怎么使用數據沖突檢測器?
在go中,已經內置數據沖突檢測器,直接在go命令行添加參數-race即可,如以下4種方式:
- go test -race mypkg //測試mypkg包
- go run -race main.go //帶沖突檢測調試運行源代碼main.go
- go build -race main.go //帶沖突檢測編譯源代碼
- go install -race mypkg//安裝mypkg包
沖突報告格式
檢測到沖突時,會按照以下格式打印沖突報告,它包含堆棧跟蹤信息,以及協程編號,如:
? dataRace git:(master) ? go run -race main.go==================WARNING:?DATA?RACE??//警告: 數據沖突Write at 0x00c000124180 by goroutine 7: //普通協程寫操作 runtime.mapassign_faststr() /usr/local/go/src/runtime/map_faststr.go:202 +0x0 main.main.func1() /Users/xb/gitlab/go/go_core_program/sync/dataRace/main.go:9 +0x5dPrevious write at 0x00c000124180 by main goroutine: //主協程寫操作 runtime.mapassign_faststr() /usr/local/go/src/runtime/map_faststr.go:202 +0x0 main.main() /Users/xb/gitlab/go/go_core_program/sync/dataRace/main.go:13 +0xcbGoroutine 7 (running) created at: main.main() /Users/xb/gitlab/go/go_core_program/sync/dataRace/main.go:8 +0x9c==================2 b1 aFound 1 data race(s) //找到一處數據沖突exit?status?66運行選項
GORACE環境變量用于設置數據沖突檢測選項, 在執行go程序前設置該值即生效,格式如下:
GORACE="選項1=值1 選項2=值2 ..."包含以下常用選項:
- log_path (default stderr): 日志文件前綴,沖突檢測結果存入log_path.pid. 特別的,如果配置為stdout則輸出到標準輸出,配置為stderr則輸出到錯誤輸出
- exitcode (default 66): 檢測到沖突時,程序的退出狀態碼,可以自定義,默認為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中等待多少毫秒后退出,默認1秒
排除單元測試
當你使用-race參數構建時,go命令定義了更多的構建標記,當你運行沖突檢測時,你可以使用這些標記排除代碼和單元測試,比如:
// +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) {// ... 包含在沖突檢測下執行耗時太長的代碼}使用沖突檢測時注意事項
當使用go test -race做沖突檢測時,檢測器只會檢測運行時的沖突,沒有執行的代碼塊不會進行檢測,如果你的單元測試是不完全覆蓋,你需要使用go build -race構建一個完整的二進制包進行檢測
典型數據沖突場景
- Race on loop counter 循環計數器沖突
package mainimport ( "fmt" "sync")func main() { //以下代碼由于并發,同時獲取值,存在沖突,所以i不會按照預期(012345)打印,比如打印55555, 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() }() }}解決辦法:對變量拷貝一份出來,新的變量指向不同的內存地址
package mainimport ( "fmt" "sync")func main() { //讀取本地拷貝值j,與i指向不同的地址,解決沖突 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()}- 由于意外,共享了變量導致沖突
func ParallelWrite(data []byte) chan error { res := make(chan error, 2) f1, err := os.Create("file1") if err != nil { res 解決辦法: 重新分配err變量
..._, err := f1.Write(data)..._, err := f2.Write(data)...- 未加保護的全局變量
var service map[string]net.Addrfunc RegisterService(name string, addr net.Addr) { service[name] = addr}func LookupService(name string) net.Addr { return service[name]}以上代碼中,map在多個協程中并發中讀寫會導致沖突
解決方法: 使用互斥鎖,保證同時只能讀或者寫
var ( service map[string]net.Addr serviceMu sync.Mutex)func RegisterService(name string, addr net.Addr) { serviceMu.Lock() defer serviceMu.Unlock() service[name] = addr}func LookupService(name string) net.Addr { serviceMu.Lock() defer serviceMu.Unlock() return service[name]}- 使用了不受保護的基本數據類型
基本數據類型,如bool, int, int64等也存在數據沖突,這種問題難以排查,一般都是由于非原子的內存訪問引起的,如:
type Watchdog struct{ last int64 }func (w *Watchdog) KeepAlive() { w.last = time.Now().UnixNano() // First conflicting access. 寫操作,與下面的讀操作構成沖突}func (w *Watchdog) Start() { go func() { for { time.Sleep(time.Second) // Second conflicting access. 這里是讀操作 if w.last < time.Now().Add(-10*time.Second).UnixNano() { fmt.Println("No keepalives for 10 seconds. Dying.") os.Exit(1) } } }()}解決方法:使用通道或者互斥鎖mutex, 也可以使用無鎖的sync/atomic包,如:
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) } } }()}- 未同步的發送和關閉操作,如:
c := make(chan struct{}) // or buffered channel 這里也可以使用帶緩沖的通道演示// The race detector cannot derive the happens before relation// for the following send and close operations. These two operations 下面的通道發送和關閉操作沒有進行同步,導致沖突// are unsynchronized and happen concurrently.go func() { c 解決方法:通道關閉前,增加一個讀取操作,完成同步
c := make(chan struct{}) // or buffered channelgo func() { c - 單例模式場景也使用鎖避免沖突
package mainimport ( "fmt" "sync" "sync/atomic")//定義單例結構體type singleton struct {}var( instance *singleton initialized uint32 //初始化標志,用于標識是否已經被初始化 mu sync.Mutex //互斥鎖對象)func Instance() *singleton{ if atomic.LoadUint32(&initialized)==1{ //如果實例已經初始化,直接返回 return instance } //如果沒有實例化,則用鎖同步執行下面的代碼,即同一時間只能有一個協程進入執行以下代碼塊 mu.Lock() defer mu.Unlock() if instance==nil{ defer atomic.StoreUint32(&initialized, 1) instance = &singleton{} } return instance}func main(){ mySingleton := Instance() fmt.Printf("單例模式得到的對象:%v", mySingleton)}- 與單例模式類似的,用鎖實現某個方法只調用一次(DoOnce)
package mainimport ( "sync" "sync/atomic")type Once struct{ m sync.Mutex done uint32}//傳入一個回調函數,保證只執行一次該回調函數func(o *Once)Do(f func()){ if atomic.LoadUint32(&o.done) == 1 { return } o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() //回調函數 }}數據沖突檢測器當前支持的系統
- linux/amd64
- linux/ppc64le
- linux/arm64
- freebsd/amd64
- netbsd/amd64
- darwin/amd64
- windows/amd64
運行時開銷
開啟沖突檢測,通常程序的內存使用會增加5~10倍,執行耗時增加2~20倍.
注意事項: 數據沖突檢測器為每個defer和recover語句分配額外8字節,該內存直到協程退出才會釋放,這意味著如果你有一個長時間運行的協程goroutine,它會周期性的調用defer和recover,導致程序內存使用持續增長,且這些內存分配不會顯示在runtime.ReadMemStats(運行時讀內存統計)和runtime/pprof(運行時性能調試工具pprof統計)中.
參考文檔:
https://golang.org/doc/articles/race_detector.html
Go語言高級編程 (Advanced Go Programming)
總結
以上是生活随笔為你收集整理的golang int64转string_Golang 并发数据冲突检测器与并发安全的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python脚本编写_【PyQGIS】编
- 下一篇: 如何培养孩子成为好学生?