最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Go并發(fā)編程實(shí)現(xiàn)數(shù)據(jù)競爭

 更新時間:2021年09月26日 09:39:46   作者:failymao  
本文主要介紹了Go并發(fā)編程實(shí)現(xiàn)數(shù)據(jù)競爭,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下

1.前言

雖然在 go 中,并發(fā)編程十分簡單, 只需要使用 go func() 就能啟動一個 goroutine 去做一些事情,但是正是由于這種簡單我們要十分當(dāng)心,不然很容易出現(xiàn)一些莫名其妙的 bug 或者是你的服務(wù)由于不知名的原因就重啟了。 而最常見的bug是關(guān)于線程安全方面的問題,比如對同一個map進(jìn)行寫操作。

2.數(shù)據(jù)競爭

線程安全是否有什么辦法檢測到呢?

答案就是 data race tag,go 官方早在 1.1 版本就引入了數(shù)據(jù)競爭的檢測工具,我們只需要在執(zhí)行測試或者是編譯的時候加上 -race 的 flag 就可以開啟數(shù)據(jù)競爭的檢測

使用方式如下

go test -race main.go
go build -race

不建議在生產(chǎn)環(huán)境 build 的時候開啟數(shù)據(jù)競爭檢測,因?yàn)檫@會帶來一定的性能損失(一般內(nèi)存5-10倍,執(zhí)行時間2-20倍),當(dāng)然 必須要 debug 的時候除外。
建議在執(zhí)行單元測試時始終開啟數(shù)據(jù)競爭的檢測

2.1 示例一

執(zhí)行如下代碼,查看每次執(zhí)行的結(jié)果是否一樣

2.1.1 測試

代碼

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() {
    // 開啟兩個 協(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í)行的時候,都使用 go routine(i) 啟動了兩個 goroutine,但是并沒有控制它的執(zhí)行順序,并不能滿足順序一致性內(nèi)存模型。

當(dāng)然由于種種不確定性,所有肯定不止這兩種情況,

2.1.2 data race 檢測

上面問題的出現(xiàn)在上線后如果出現(xiàn)bug會非常難定位,因?yàn)椴恢赖降资悄睦锍霈F(xiàn)了問題,所以我們就要在測試階段就結(jié)合 data race 工具提前發(fā)現(xiàn)問題。

使用

go run -race ./main.go

輸出: 運(yùn)行結(jié)果發(fā)現(xiàn)輸出記錄太長,調(diào)試的時候并不直觀,結(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 行這個地方有一個 goroutine 在讀取數(shù)據(jù),但是呢,在 33 行這個地方又有一個 goroutine 在寫入,所以產(chǎn)生了數(shù)據(jù)競爭。
然后下面分別說明這兩個 goroutine 是什么時候創(chuàng)建的,已經(jīng)當(dāng)前是否在運(yùn)行當(dāng)中。

2.2 循環(huán)中使用goroutine引用臨時變量

代碼如下:

func main() {
 var wg sync.WaitGroup
 wg.Add(5)
 for i := 0; i < 5; i++ {
  go func() {
   fmt.Println(i) 
   wg.Done()
  }()
 }
    wg.Wait()
}

輸出:常見的答案就是會輸出 5 個 5,因?yàn)樵?for 循環(huán)的 i++ 會執(zhí)行的快一些,所以在最后打印的結(jié)果都是 5
這個答案不能說不對,因?yàn)檎娴膱?zhí)行的話大概率也是這個結(jié)果,但是不全。因?yàn)檫@里本質(zhì)上是有數(shù)據(jù)競爭,在新啟動的 goroutine 當(dāng)中讀取 i 的值,在 main 中寫入,導(dǎo)致出現(xiàn)了 data race,這個結(jié)果應(yīng)該是不可預(yù)知的,因?yàn)槲覀儾荒芗俣?goroutine 中 print 就一定比外面的 i++ 慢,習(xí)慣性的做這種假設(shè)在并發(fā)編程中是很有可能會出問題的

正確示例:將 i 作為參數(shù)傳入即可,這樣每個 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)建/寫入第一個文件
 f1, err := os.Create("/tmp/file1")
 
 if err != nil {
  res <- err
 } else {
  go func() {
   // 下面的這個函數(shù)在執(zhí)行時,是使用err進(jìn)行判斷,但是err的變量是個共享的變量
   _, err = f1.Write(data)
   res <- err
   f1.Close()
  }()
 }
 
  // 創(chuàng)建寫入第二個文件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)錯的地方是,21 行和 28 行,有 data race,這里主要是因?yàn)楣蚕砹?err 這個變量

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

修正: 在兩個goroutine中使用新的臨時變量

_, err := f1.Write(data)
...
_, err := f2.Write(data)
...

2.4 不受保護(hù)的全局變量

所謂全局變量是指,定義在多個函數(shù)的作用域之外,可以被多個函數(shù)或方法進(jìn)行調(diào)用,常用的如 map數(shù)據(jù)類型

// 定義一個全局變量 map數(shù)據(jù)類型
var service = map[string]string{}
 
// RegisterService RegisterService
// 用于寫入或更新key-value
func RegisterService(name, addr string) {
 service[name] = addr
}
 
// LookupService LookupService
// 用于查詢某個key-value
func LookupService(name string) string {
 return service[name]
}

要寫出可測性比較高的代碼就要少用或者是盡量避免用全局變量,使用 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)體的某個字段。 如下一段代碼

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)行判斷的時候,很可能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ù)競爭

一個很有趣的例子 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()
 }
}

這個例子有趣的點(diǎn)在于,最后輸出的結(jié)果會有這種例子

Ben says, "Hello my name is Jerry"
Ben says, "Hello my name is Jerry"

這是因?yàn)槲覀冊趍aker = jerry這種賦值操作的時候并不是原子的,在上一篇文章中我們講到過,只有對 single machine word 進(jìn)行賦值的時候才是原子的,雖然這個看上去只有一行,但是 interface 在 go 中其實(shí)是一個結(jié)構(gòu)體,它包含了 type 和 data 兩個部分,所以它的復(fù)制也不是原子的,會出現(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
}

這個案例有趣的點(diǎn)還在于,這個案例的兩個結(jié)構(gòu)體的內(nèi)存布局一模一樣所以出現(xiàn)錯誤也不會 panic 退出,如果在里面再加入一個 string 的字段,去讀取就會導(dǎo)致 panic,但是這也恰恰說明這個案例很可怕,這種錯誤在線上實(shí)在太難發(fā)現(xiàn)了,而且很有可能會很致命。

3. 總結(jié)

使用 go build -race main.go和go test -race ./ 可以測試程序代碼中是否存在數(shù)據(jù)競爭問題

  • 善用 data race 這個工具幫助我們提前發(fā)現(xiàn)并發(fā)錯誤
  • 不要對未定義的行為做任何假設(shè),雖然有時候我們寫的只是一行代碼,但是 go 編譯器可能后面做了很多事情,并不是說一行寫完就一定是原子的
  • 即使是原子的出現(xiàn)了 data race 也不能保證安全,因?yàn)槲覀冞€有可見性的問題,上篇我們講到了現(xiàn)代的 cpu 基本上都會有一些緩存的操作。
  • 所有出現(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ù)競爭的文章就介紹到這了,更多相關(guān)Go 數(shù)據(jù)競爭內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Go如何優(yōu)雅的使用字節(jié)池示例詳解

    Go如何優(yōu)雅的使用字節(jié)池示例詳解

    在編程開發(fā)中,我們經(jīng)常會需要頻繁創(chuàng)建和銷毀同類對象的情形,這樣的操作很可能會對性能造成影響,這時常用的優(yōu)化手段就是使用對象池(object pool),這篇文章主要給大家介紹了關(guān)于Go如何優(yōu)雅的使用字節(jié)池的相關(guān)資料,需要的朋友可以參考下
    2022-08-08
  • Go語言中的反射原理解析與應(yīng)用

    Go語言中的反射原理解析與應(yīng)用

    反射(Reflection)是計(jì)算機(jī)科學(xué)中的一個重要概念,它允許程序在運(yùn)行時檢查變量和值,獲取它們的類型信息,并且能夠修改它們,本文將結(jié)合實(shí)際案例,詳細(xì)介紹Go語言中反射的基本概念、關(guān)鍵函數(shù)以及使用場景,需要的朋友可以參考下
    2024-10-10
  • Golang 的defer執(zhí)行規(guī)則說明

    Golang 的defer執(zhí)行規(guī)則說明

    這篇文章主要介紹了Golang 的defer執(zhí)行規(guī)則說明,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • 基于Golang實(shí)現(xiàn)Redis協(xié)議解析器

    基于Golang實(shí)現(xiàn)Redis協(xié)議解析器

    這篇文章主要為大家詳細(xì)介紹了如何通過GO語言編寫簡單的Redis協(xié)議解析器,文中的示例代碼講解詳細(xì),對我們深入了解Go語言有一定的幫助,需要的可以參考一下
    2023-03-03
  • go實(shí)現(xiàn)grpc四種數(shù)據(jù)流模式

    go實(shí)現(xiàn)grpc四種數(shù)據(jù)流模式

    這篇文章主要為大家介紹了go實(shí)現(xiàn)grpc四種數(shù)據(jù)流模式,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪
    2022-04-04
  • 解決Golang小數(shù)float64在實(shí)際工程中加減乘除的精度問題

    解決Golang小數(shù)float64在實(shí)際工程中加減乘除的精度問題

    這篇文章主要介紹了解決Golang小數(shù)float64在實(shí)際工程中加減乘除的精度問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-03-03
  • go函數(shù)的參數(shù)設(shè)置默認(rèn)值的方法

    go函數(shù)的參數(shù)設(shè)置默認(rèn)值的方法

    Go語言不直接支持函數(shù)參數(shù)默認(rèn)值,但可以通過指針、結(jié)構(gòu)體、變長參數(shù)和選項(xiàng)模式等方法模擬,下面給大家分享幾種方式模擬函數(shù)參數(shù)的默認(rèn)值功能,感興趣的朋友一起看看吧
    2025-01-01
  • golang 生成定單號的操作

    golang 生成定單號的操作

    這篇文章主要介紹了golang 生成定單號的操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • Golang限流庫與漏桶和令牌桶的使用介紹

    Golang限流庫與漏桶和令牌桶的使用介紹

    這篇文章主要介紹了golang限流庫以及漏桶與令牌桶的實(shí)現(xiàn)原理,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧
    2023-03-03
  • Go語言范圍Range的具體使用

    Go語言范圍Range的具體使用

    range關(guān)鍵字在for循環(huán)中用于遍歷數(shù)組,切片,通道或映射的項(xiàng)目,本文主要介紹了Go語言范圍Range的具體使用,具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-01-01

最新評論

庆安县| 灵山县| 门源| 镇雄县| 濮阳县| 湛江市| 靖远县| 沅陵县| 博客| 房山区| 湖口县| 花垣县| 灌阳县| 巢湖市| 汤阴县| 唐河县| 武清区| 凯里市| 监利县| 武川县| 修文县| 闽清县| 嘉兴市| 榆树市| 德钦县| 东辽县| 旺苍县| 铁岭县| 北流市| 金塔县| 延寿县| 武穴市| 密云县| 漳平市| 余姚市| 洛川县| 徐水县| 柳河县| 双牌县| 怀集县| 临潭县|