這篇“go語(yǔ)言中常見(jiàn)的并發(fā)編程錯(cuò)誤有哪些”文章的知識(shí)點(diǎn)大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細(xì),步驟清晰,具有一定的借鑒價(jià)值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來(lái)看看這篇“go語(yǔ)言中常見(jiàn)的并發(fā)編程錯(cuò)誤有哪些”文章吧。
創(chuàng)新互聯(lián)主要從事網(wǎng)站設(shè)計(jì)制作、網(wǎng)站設(shè)計(jì)、網(wǎng)頁(yè)設(shè)計(jì)、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)南崗,10年網(wǎng)站建設(shè)經(jīng)驗(yàn),價(jià)格優(yōu)惠、服務(wù)專業(yè),歡迎來(lái)電咨詢建站服務(wù):18982081108
Go 是一個(gè)內(nèi)置支持并發(fā)編程的語(yǔ)言。借助使用 go
關(guān)鍵字去創(chuàng)建協(xié)程(輕量級(jí)線程)和在 Go 中提供的 使用 信道 和 其它的并發(fā) 同步方法,使得并發(fā)編程變得很容易、很靈活和很有趣。
另一方面,Go 并不會(huì)阻止一些因 Go 程序員粗心大意或者缺乏經(jīng)驗(yàn)而造成的并發(fā)編程錯(cuò)誤。在本文的下面部分將展示一些在 Go 編程中常見(jiàn)的并發(fā)編程錯(cuò)誤,以幫助 Go 程序員們避免再犯類似的錯(cuò)誤。
代碼行或許 不是按出現(xiàn)的順序運(yùn)行的。
在下面的程序中有兩個(gè)錯(cuò)誤。
***,在 main
協(xié)程中讀取 b
和在新的 協(xié)程 中寫入 b
可能導(dǎo)致數(shù)據(jù)爭(zhēng)用。
第二,條件 b == true
并不能保證在 main
協(xié)程 中的 a != nil
。在新的協(xié)程中編譯器和 CPU 可能會(huì)通過(guò) 重排序指令 進(jìn)行優(yōu)化,因此,在運(yùn)行時(shí) b
賦值可能發(fā)生在 a
賦值之前,在 main
協(xié)程 中當(dāng) a
被修改后,它將會(huì)讓部分 a
一直保持為 nil
。
package main import ( "time" "runtime") func main() { var a []int // nil var b bool // false // a new goroutine go func () { a = make([]int, 3) b = true // write b }() for !b { // read b time.Sleep(time.Second) runtime.Gosched() } a[0], a[1], a[2] = 0, 1, 2 // might panic}
上面的程序或者在一臺(tái)計(jì)算機(jī)上運(yùn)行的很好,但是在另一臺(tái)上可能會(huì)引發(fā)異常。或者它可能運(yùn)行了 N 次都很好,但是可能在第 (N+1) 次引發(fā)了異常。
我們將使用 sync
標(biāo)準(zhǔn)包中提供的信道或者同步方法去確保內(nèi)存中的順序。例如,
package main func main() { var a []int = nil c := make(chan struct{}) // a new goroutine go func () { a = make([]int, 3) c <- struct{}{} }() <-c a[0], a[1], a[2] = 0, 1, 2}
time.Sleep
調(diào)用去做同步我們先來(lái)看一個(gè)簡(jiǎn)單的例子。
package main import ( "fmt" "time") func main() { var x = 123 go func() { x = 789 // write x }() time.Sleep(time.Second) fmt.Println(x) // read x}
我們預(yù)期程序?qū)⒋蛴〕?nbsp;789
。如果我們運(yùn)行它,通常情況下,它確定打印的是 789
。但是,這個(gè)程序使用的同步方式好嗎?No!原因是 Go 運(yùn)行時(shí)并不保證 x
的寫入一定會(huì)發(fā)生在 x
的讀取之前。在某些條件下,比如在同一個(gè)操作系統(tǒng)上,大部分 CPU 資源被其它運(yùn)行的程序所占用的情況下,寫入 x
可能就會(huì)發(fā)生在讀取 x
之后。這就是為什么我們?cè)谡降捻?xiàng)目中,從來(lái)不使用 time.Sleep
調(diào)用去實(shí)現(xiàn)同步的原因。
我們來(lái)看一下另外一個(gè)示例。
package main import ( "fmt" "time") var x = 0 func main() { var num = 123 var p = &num c := make(chan int) go func() { c <- *p + x }() time.Sleep(time.Second) num = 789 fmt.Println(<-c)}
你認(rèn)為程序的預(yù)期輸出是什么?123
還是 789
?事實(shí)上它的輸出與編譯器有關(guān)。對(duì)于標(biāo)準(zhǔn)的 Go 編譯器 1.10 來(lái)說(shuō),這個(gè)程序很有可能輸出是 123
。但是在理論上,它可能輸出的是 789
,或者其它的隨機(jī)數(shù)。
現(xiàn)在,我們來(lái)改變 c <- *p + x
為 c <- *p
,然后再次運(yùn)行這個(gè)程序。你將會(huì)發(fā)現(xiàn)輸出變成了 789
(使用標(biāo)準(zhǔn)的 Go 編譯器 1.10)。這再次說(shuō)明它的輸出是與編譯器相關(guān)的。
是的,在上面的程序中存在數(shù)據(jù)爭(zhēng)用。表達(dá)式 *p
可能會(huì)被先計(jì)算、后計(jì)算、或者在處理賦值語(yǔ)句 num = 789
時(shí)計(jì)算。time.Sleep
調(diào)用并不能保證 *p
發(fā)生在賦值語(yǔ)句處理之前進(jìn)行。
對(duì)于這個(gè)特定的示例,我們將在新的協(xié)程創(chuàng)建之前,將值保存到一個(gè)臨時(shí)值中,然后在新的協(xié)程中使用臨時(shí)值去消除數(shù)據(jù)爭(zhēng)用。
... tmp := *p + x go func() { c <- tmp }()...
掛起協(xié)程是指讓協(xié)程一直處于阻塞狀態(tài)。導(dǎo)致協(xié)程被掛起的原因很多。比如,
一個(gè)協(xié)程嘗試從一個(gè) nil 信道中或者從一個(gè)沒(méi)有其它協(xié)程給它發(fā)送值的信道中檢索數(shù)據(jù)。
一個(gè)協(xié)程嘗試去發(fā)送一個(gè)值到 nil 信道,或者發(fā)送到一個(gè)沒(méi)有其它的協(xié)程接收值的信道中。
一個(gè)協(xié)程被它自己死鎖。
一組協(xié)程彼此死鎖。
當(dāng)運(yùn)行一個(gè)沒(méi)有 default
分支的 select
代碼塊時(shí),一個(gè)協(xié)程被阻塞,以及在 select
代碼塊中 case
關(guān)鍵字后的所有信道操作保持阻塞狀態(tài)。
除了有時(shí)我們?yōu)榱吮苊獬绦蛲顺?,特意讓一個(gè)程序中的 main
協(xié)程保持掛起之外,大多數(shù)其它的協(xié)程掛起都是意外情況。Go 運(yùn)行時(shí)很難判斷一個(gè)協(xié)程到底是處于掛起狀態(tài)還是臨時(shí)阻塞。因此,Go 運(yùn)行時(shí)并不會(huì)去釋放一個(gè)掛起的協(xié)程所占用的資源。
在 誰(shuí)先響應(yīng)誰(shuí)獲勝 的信道使用案例中,如果使用的 future 信道容量不夠大,當(dāng)嘗試向 Future 信道發(fā)送結(jié)果時(shí),一些響應(yīng)較慢的信道將被掛起。比如,如果調(diào)用下面的函數(shù),將有 4 個(gè)協(xié)程處于永遠(yuǎn)阻塞狀態(tài)。
func request() int { c := make(chan int) for i := 0; i < 5; i++ { i := i go func() { c <- i // 4 goroutines will hang here. }() } return <-c}
為避免這 4 個(gè)協(xié)程一直處于掛起狀態(tài), c
信道的容量必須至少是 4
。
在 實(shí)現(xiàn)誰(shuí)先響應(yīng)誰(shuí)獲勝的第二種方法 的信道使用案例中,如果將 future 信道用做非緩沖信道,那么有可能這個(gè)信息將永遠(yuǎn)也不會(huì)有響應(yīng)而掛起。例如,如果在一個(gè)協(xié)程中調(diào)用下面的函數(shù),協(xié)程可能會(huì)掛起。原因是,如果接收操作 <-c
準(zhǔn)備就緒之前,五個(gè)發(fā)送操作全部嘗試發(fā)送,那么所有的嘗試發(fā)送的操作將全部失敗,因此那個(gè)調(diào)用者協(xié)程將永遠(yuǎn)也不會(huì)接收到值。
func request() int { c := make(chan int) for i := 0; i < 5; i++ { i := i go func() { select { case c <- i: default: } }() } return <-c}
將信道 c
變成緩沖信道將保證五個(gè)發(fā)送操作中的至少一個(gè)操作會(huì)發(fā)送成功,這樣,上面函數(shù)中的那個(gè)調(diào)用者協(xié)程將不會(huì)被掛起。
sync
標(biāo)準(zhǔn)包中拷貝類型值在實(shí)踐中,sync
標(biāo)準(zhǔn)包中的類型值不會(huì)被拷貝。我們應(yīng)該只拷貝這個(gè)值的指針。
下面是一個(gè)錯(cuò)誤的并發(fā)編程示例。在這個(gè)示例中,當(dāng)調(diào)用 Counter.Value
方法時(shí),將拷貝一個(gè) Counter
接收值。作為接收值的一個(gè)字段,Counter
接收值的各個(gè) Mutex
字段也會(huì)被拷貝??截惒皇峭桨l(fā)生的,因此,拷貝的 Mutex
值可能會(huì)出錯(cuò)。即便是沒(méi)有錯(cuò)誤,拷貝的 Counter
接收值的訪問(wèn)保護(hù)也是沒(méi)有意義的。
import "sync" type Counter struct { sync.Mutex n int64} // This method is okay.func (c *Counter) Increase(d int64) (r int64) { c.Lock() c.n += d r = c.n c.Unlock() return} // The method is bad. When it is called, a Counter// receiver value will be copied.func (c Counter) Value() (r int64) { c.Lock() r = c.n c.Unlock() return}
我們只需要改變 Value
接收類型方法為指針類型 *Counter
,就可以避免拷貝 Mutex
值。
在官方的 Go SDK 中提供的 go vet
命令將會(huì)報(bào)告潛在的錯(cuò)誤值拷貝。
sync.WaitGroup
的方法每個(gè) sync.WaitGroup
值維護(hù)一個(gè)內(nèi)部計(jì)數(shù)器,這個(gè)計(jì)數(shù)器的初始值為 0。如果一個(gè) WaitGroup
計(jì)數(shù)器的值是 0,調(diào)用 WaitGroup
值的 Wait
方法就不會(huì)被阻塞,否則,在計(jì)數(shù)器值為 0 之前,這個(gè)調(diào)用會(huì)一直被阻塞。
為了讓 WaitGroup
值的使用有意義,當(dāng)一個(gè) WaitGroup
計(jì)數(shù)器值為 0 時(shí),必須在相應(yīng)的 WaitGroup
值的 Wait
方法調(diào)用之前,去調(diào)用 WaitGroup
值的 Add
方法。
例如,下面的程序中,在不正確位置調(diào)用了 Add
方法,這將使***打印出的數(shù)字不總是 100
。事實(shí)上,這個(gè)程序***打印的數(shù)字可能是在 [0, 100)
范圍內(nèi)的一個(gè)隨意數(shù)字。原因就是 Add
方法的調(diào)用并不保證一定會(huì)發(fā)生在 Wait
方法調(diào)用之前。
package main import ( "fmt" "sync" "sync/atomic") func main() { var wg sync.WaitGroup var x int32 = 0 for i := 0; i < 100; i++ { go func() { wg.Add(1) atomic.AddInt32(&x, 1) wg.Done() }() } fmt.Println("To wait ...") wg.Wait() fmt.Println(atomic.LoadInt32(&x))}
為讓程序的表現(xiàn)符合預(yù)期,在 for
循環(huán)中,我們將把 Add
方法的調(diào)用移動(dòng)到創(chuàng)建的新協(xié)程的范圍之外,修改后的代碼如下。
... for i := 0; i < 100; i++ { wg.Add(1) go func() { atomic.AddInt32(&x, 1) wg.Done() }() }...
在 信道使用案例 的文章中,我們知道一些函數(shù)將返回 futures 信道。假設(shè) fa
和 fb
就是這樣的兩個(gè)函數(shù),那么下面的調(diào)用就使用了不正確的 future 參數(shù)。
doSomethingWithFutureArguments(<-fa(), <-fb())
在上面的代碼行中,兩個(gè)信道接收操作是順序進(jìn)行的,而不是并發(fā)的。我們做如下修改使它變成并發(fā)操作。
ca, cb := fa(), fb()doSomethingWithFutureArguments(<-c1, <-c2)
Go 程序員經(jīng)常犯的一個(gè)錯(cuò)誤是,還有一些其它的協(xié)程可能會(huì)發(fā)送值到以前的信道時(shí),這個(gè)信道就已經(jīng)被關(guān)閉了。當(dāng)這樣的發(fā)送(發(fā)送到一個(gè)已經(jīng)關(guān)閉的信道)真實(shí)發(fā)生時(shí),將引發(fā)一個(gè)異常。
這種錯(cuò)誤在一些以往的著名 Go 項(xiàng)目中也有發(fā)生,比如在 Kubernetes 項(xiàng)目中的 這個(gè) bug 和 這個(gè) bug。
如何安全和優(yōu)雅地關(guān)閉信道,請(qǐng)閱讀 這篇文章。
到目前為止(Go 1.10),在標(biāo)準(zhǔn)的 Go 編譯器中,在一個(gè) 64 位原子操作中涉及到的值的地址要求必須是 64 位對(duì)齊的。如果沒(méi)有對(duì)齊則導(dǎo)致當(dāng)前的協(xié)程異常。對(duì)于標(biāo)準(zhǔn)的 Go 編譯器來(lái)說(shuō),這種失敗僅發(fā)生在 32 位的架構(gòu)上。請(qǐng)閱讀 內(nèi)存布局 去了解如何在一個(gè) 32 位操作系統(tǒng)上保證 64 位對(duì)齊。
time.After
函數(shù)調(diào)用占用在 time
標(biāo)準(zhǔn)包中的 After
函數(shù)返回 一個(gè)延遲通知的信道。這個(gè)函數(shù)在某些情況下用起來(lái)很便捷,但是,每次調(diào)用它將創(chuàng)建一個(gè) time.Timer
類型的新值。這個(gè)新創(chuàng)建的 Timer
值在通過(guò)傳遞參數(shù)到 After
函數(shù)指定期間保持激活狀態(tài),如果在這個(gè)期間過(guò)多的調(diào)用了該函數(shù),可能會(huì)有太多的 Timer
值保持激活,這將占用大量的內(nèi)存和計(jì)算資源。
例如,如果調(diào)用了下列的 longRunning
函數(shù),將在一分鐘內(nèi)產(chǎn)生大量的消息,然后在某些周期內(nèi)將有大量的 Timer
值保持激活,即便是大量的這些 Timer
值已經(jīng)沒(méi)用了也是如此。
import ( "fmt" "time") // The function will return if a message arrival interval// is larger than one minute.func longRunning(messages <-chan string) { for { select { case <-time.After(time.Minute): return case msg := <-messages: fmt.Println(msg) } }}
為避免在上述代碼中創(chuàng)建過(guò)多的 Timer
值,我們將使用一個(gè)單一的 Timer
值去完成同樣的任務(wù)。
func longRunning(messages <-chan string) { timer := time.NewTimer(time.Minute) defer timer.Stop() for { select { case <-timer.C: return case msg := <-messages: fmt.Println(msg) if !timer.Stop() { <-timer.C } } // The above "if" block can also be put here. timer.Reset(time.Minute) }}
time.Timer
值在***,我們將展示一個(gè)符合語(yǔ)言使用習(xí)慣的 time.Timer
值的使用示例。需要注意的一個(gè)細(xì)節(jié)是,那個(gè) Reset
方法總是在停止或者 time.Timer
值釋放時(shí)被使用。
在 select
塊的***個(gè) case
分支的結(jié)束部分,time.Timer
值被釋放,因此,我們不需要去停止它。但是必須在第二個(gè)分支中停止定時(shí)器。如果在第二個(gè)分支中 if
代碼塊缺失,它可能至少在 Reset
方法調(diào)用時(shí),會(huì)(通過(guò) Go 運(yùn)行時(shí))發(fā)送到 timer.C
信道,并且那個(gè) longRunning
函數(shù)可能會(huì)早于預(yù)期返回,對(duì)于 Reset
方法來(lái)說(shuō),它可能僅僅是重置內(nèi)部定時(shí)器為 0,它將不會(huì)清理(耗盡)那個(gè)發(fā)送到 timer.C
信道的值。
例如,下面的程序很有可能在一秒內(nèi)而不是十秒時(shí)退出。并且更重要的是,這個(gè)程序并不是 DRF 的(LCTT 譯注:data race free,多線程程序的一種同步程度)。
package main import ( "fmt" "time") func main() { start := time.Now() timer := time.NewTimer(time.Second/2) select { case <-timer.C: default: time.Sleep(time.Second) // go here } timer.Reset(time.Second * 10) <-timer.C fmt.Println(time.Since(start)) // 1.000188181s}
當(dāng) time.Timer
的值不再被其它任何一個(gè)東西使用時(shí),它的值可能被停留在一種非停止?fàn)顟B(tài),但是,建議在結(jié)束時(shí)停止它。
在多個(gè)協(xié)程中如果不按建議使用 time.Timer
值并發(fā),可能會(huì)有 bug 隱患。
我們不應(yīng)該依賴一個(gè) Reset
方法調(diào)用的返回值。Reset
方法返回值的存在僅僅是為了兼容性目的。
以上就是關(guān)于“go語(yǔ)言中常見(jiàn)的并發(fā)編程錯(cuò)誤有哪些”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對(duì)大家有幫助,若想了解更多相關(guān)的知識(shí)內(nèi)容,請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。
網(wǎng)頁(yè)題目:go語(yǔ)言中常見(jiàn)的并發(fā)編程錯(cuò)誤有哪些
當(dāng)前URL:http://www.rwnh.cn/article6/jjecog.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站制作、面包屑導(dǎo)航、電子商務(wù)、企業(yè)建站、網(wǎng)站維護(hù)、網(wǎng)站排名
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)