<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          理解真實(shí)世界中 Go 的并發(fā) BUG

          共 4559字,需瀏覽 10分鐘

           ·

          2020-10-14 09:03

          點(diǎn)擊上方藍(lán)色“Go語言中文網(wǎng)”關(guān)注,回復(fù)「電子書」領(lǐng)全套Go資料

          有幾個(gè)學(xué)生研究歸納了go編程中的并發(fā)bugs,發(fā)表了一篇(英文)論文:《Understanding Real-World Concurrency Bugs in Go》。為你下載好了 PDF,關(guān)注公眾號 Go語言中文網(wǎng),回復(fù) gostudy 獲取。

          在此做一個(gè)筆記,便于查閱。

          文章以六個(gè)產(chǎn)品級go應(yīng)用作為研究對象:Docker、Kubernetes、etcd、gRPC、CockroachDB、BoltDB,總共研究了這些應(yīng)用中的171個(gè)bug,研究它們的根本原因,并重現(xiàn)這些bugs,以及檢查它們的修復(fù)補(bǔ)丁。最后用兩個(gè)現(xiàn)有g(shù)o并發(fā)bug檢測器測試了這些bug。

          文章試圖回答一個(gè)問題:對于兩種線程/協(xié)程間通信機(jī)制,消息傳遞機(jī)制和共享內(nèi)存機(jī)制,哪個(gè)更不容易出錯(cuò)?

          文章從兩個(gè)維度對bug進(jìn)行了分類,bug原因(對共享內(nèi)存的誤用、對消息傳遞的誤用)和bug表現(xiàn)(阻塞性bug、非阻塞性bug)。

          研究結(jié)果及提交日志可以在以下地址查閱:https://github.com/system-pclub/go-concurrency-bugs

          many concurrency bugs are caused by the mixed usage of message passing and other new semantics and new libraries in Go, which can easily be overlooked but hard to detect.

          背景

          使用共享內(nèi)存實(shí)現(xiàn)同步

          Go支持協(xié)程間共享內(nèi)存,提供了多種傳統(tǒng)的同步手段,如鎖(Mutex)、讀寫鎖(RWMutex)、條件變量(Cond)、原子讀寫(atomic)。go的RWMutex實(shí)現(xiàn)與C中的pthread_rwlock_t不同,go中的寫鎖請求優(yōu)先級高于讀鎖。

          go中還有一些新特性,Once保證一個(gè)函數(shù)只執(zhí)行一次:使用 Once.Do(f) 方法,即使這一語句被多個(gè)協(xié)程調(diào)用了多次,也只有第一次的時(shí)候,函數(shù)f會被執(zhí)行。

          和C中的pthread_join類似,go使用WaitGroup來實(shí)現(xiàn)等待協(xié)程對其他協(xié)程的等待。

          使用消息傳遞實(shí)現(xiàn)同步

          channel(chan)是go的新特性,學(xué)習(xí)go語言編程的都應(yīng)該熟悉了。channel分有緩沖和無緩沖兩種(buffered and unbuffered)。

          使用select可以從多路channel中進(jìn)行選擇。當(dāng)有多路case有效時(shí),select會從中隨機(jī)選擇一個(gè)去執(zhí)行,這種隨機(jī)性可能會造成bug。

          Go引入了幾種新機(jī)制來簡化協(xié)程間的交互,如用context攜帶數(shù)據(jù)傳遞在不同協(xié)程之間,還有Pipe可在讀協(xié)程和寫協(xié)程之間傳遞流式數(shù)據(jù)。這兩種都是新的消息傳遞機(jī)制,不注意的話可能引起新的并發(fā)bug。

          Go并發(fā)模型

          在研究并發(fā)bug前,文章先研究了go中的并發(fā)模型。

          首先統(tǒng)計(jì)了那幾個(gè)應(yīng)用中創(chuàng)建gorutine的(靜態(tài))語句數(shù)量(位置數(shù)量),如下表:

          img

          文章覺得喜歡用匿名函數(shù)創(chuàng)建gorutine的多些(除了kubernetes和BoltDB),另外還發(fā)現(xiàn)C語言版gRPC比go語言版更少創(chuàng)建線程語句。

          然后,文章還統(tǒng)計(jì)了各種同步機(jī)制的使用比例,如下圖:

          img

          從中可以看出,共享內(nèi)存機(jī)制的鎖還是用得最多啊!

          同時(shí),這些機(jī)制的使用比例,隨著項(xiàng)目時(shí)間推進(jìn),是否有什么變化趨勢的?似乎沒有明顯變化,如下截圖:

          img

          Bug分類

          分類如下:

          img

          從數(shù)值看,阻塞性bug和非阻塞性bug出現(xiàn)數(shù)量差不多。

          (筆者注:對于原因而言,從數(shù)值上看使用共享內(nèi)存的造成bug比較多,但是這里只統(tǒng)計(jì)了絕對值,沒有和前面共享機(jī)制的使用量結(jié)合起來考慮比例,似乎不大妥當(dāng)。)

          對于這些bug,文章作者使用相應(yīng)有bug的版本,根據(jù)bug報(bào)告中的操作嘗試重現(xiàn)這些bug,結(jié)果發(fā)現(xiàn)并發(fā)bug是很難重現(xiàn)的。從而這些bug存在時(shí)間都比較長,而一旦被發(fā)現(xiàn),一般會比較快地得到解決。bug生存時(shí)間統(tǒng)計(jì)如下:

          img

          Bug原因分析

          1、阻塞性bug

          統(tǒng)計(jì)如下:

          img

          具體分析

          (1)對共享內(nèi)存保護(hù)的失誤:

          Mutex:28個(gè)阻塞性bug由對鎖的不當(dāng)使用造成,包括重復(fù)鎖、以沖突的順序申請鎖、忘記解鎖*。這些bug都是傳統(tǒng)bug,文章覺得傳統(tǒng)的死鎖檢測算法應(yīng)該能檢測出這類bug。

          RWMutex:前面提到過,go中的寫鎖優(yōu)先級高。這種實(shí)現(xiàn)機(jī)制可以造成如下bug:協(xié)程A對同一個(gè)RWMutex申請兩次讀鎖,但在這兩次申請中間,協(xié)程B申請寫鎖。此時(shí),由于A已經(jīng)持有了一個(gè)讀鎖,而寫鎖又是排他性的,所以B被阻塞。然后,A第二次申請讀鎖時(shí),由于B的寫鎖優(yōu)先級高,所以A的讀鎖必須排在B的寫鎖請求之后,導(dǎo)致A被阻塞。從而發(fā)生了死鎖。

          統(tǒng)計(jì)中有5個(gè)bug是由這個(gè)原因造成。由于在C語言中這種情況不會造成死鎖,所以參考C語言類似機(jī)制在Go中寫這樣的代碼,容易導(dǎo)致這樣的bug。

          Wait:3個(gè)阻塞性bug歸因于等待操作無法繼續(xù)。跟Mutex和RWMutex不同,這里并不涉及循環(huán)等待。有兩個(gè)bug是這樣的:Cond被用來保護(hù)共享內(nèi)存訪問,其中一個(gè)協(xié)程調(diào)用了Cond.Wait(),但是在這之后卻沒有別的協(xié)程調(diào)用Cond.Signal()(或Cond.Broadcast())。

          另一個(gè)bug,Docker#25384,如下圖所示,使用了一個(gè)共享的WaitGroup變量,造成bug主要是Wait()放在了錯(cuò)誤的地方即第7行,修復(fù)bug只需要把Wait()挪到圖中的第8行(循環(huán)外)。

          img

          (2)對消息傳遞的誤用

          Channel:對通過channel傳遞消息的錯(cuò)誤使用導(dǎo)致了29個(gè)阻塞性bug。很多都跟發(fā)送和接收的錯(cuò)配有關(guān)。如下圖所示,在使用第2行代碼初始化channel的情況下,在子協(xié)程執(zhí)行到第6行代碼前,如果超時(shí)時(shí)間到了,或者子協(xié)程執(zhí)行到第6行時(shí),select的兩個(gè)case同時(shí)可用,由于select的隨機(jī)性而跑到了超時(shí)的那個(gè)case,就會導(dǎo)致finishReq函數(shù)返回,從而子協(xié)程阻塞。這個(gè)問題的修復(fù)方法是將channel定義為緩沖channel,這樣無論何種情況子協(xié)程都不會阻塞住。

          img

          當(dāng)組合使用go特定類庫時(shí),channel的創(chuàng)建和協(xié)程阻塞有可能被埋在了類庫的調(diào)用之中。如下圖所示,行1創(chuàng)建了一個(gè)新的context對象 hcancel,同時(shí)一個(gè)新的協(xié)程被創(chuàng)建,消息可以通過hcancel的channel傳遞到新協(xié)程。如果在行4 timeout大于0,另一個(gè)context對象在行5被創(chuàng)建,并且hcancel指向了新的對象。之后,將無法向協(xié)程所關(guān)聯(lián)的舊對象發(fā)送消息,舊對象也沒法被關(guān)閉。這個(gè)問題的避免方法是,避免創(chuàng)建額外的context對象。

          img

          Channel和其他的阻塞特性:有16個(gè)bugs,其中一個(gè)協(xié)程阻塞在Channel操作,而別的協(xié)程阻塞在鎖或等待上。如下圖,協(xié)程1在發(fā)送消息到ch時(shí)阻塞了,而同時(shí)協(xié)程2卻被m.Lock()阻塞。解決方案是對協(xié)程1使用具有default分支的select來確保ch不再阻塞。

          img

          消息庫函數(shù):go提供了幾種傳遞消息和數(shù)據(jù)的庫,如Pipe。對這些的不正確使用也會造成bug。例如,和Channel類似,如果一個(gè)Pipe未關(guān)閉,Pipe的兩端一個(gè)伙伴掛了,另一個(gè)伙伴等著讀或?qū)憯?shù)據(jù),那這是等著讀或?qū)憯?shù)據(jù)的伙伴就被阻塞住了。類似的bug有4個(gè)。

          最后,關(guān)于阻塞性bug,文章認(rèn)為消息傳遞機(jī)制更容易造成更多類型的bug。

          2、非阻塞性bug

          統(tǒng)計(jì)如下:

          img

          (1)對共享內(nèi)存的保護(hù)失敗

          已有很多研究發(fā)現(xiàn),未保護(hù)共享內(nèi)存或保護(hù)錯(cuò)誤是造成數(shù)據(jù)競爭或其他非阻塞性bug的主要原因。本文也發(fā)現(xiàn)80%非阻塞性bug都?xì)w因于未保護(hù)或錯(cuò)誤地保護(hù)共享內(nèi)存。但go中的情況和傳統(tǒng)編程語言的情況也并非完全相同。

          傳統(tǒng)bug:超過一半非阻塞性bug都是由于傳統(tǒng)問題造成的,就跟在Java、C這些編程語言中一樣,如原子操作的破壞、順序混亂、數(shù)據(jù)競爭。有幾個(gè)bug是對go新特性的不夠理解造成的,如:Docker#22985 和 CockroachDB#6111 是由于將一個(gè)變量的引用通過Channel在不同協(xié)程間傳遞,從而造成了共享變量的競爭狀態(tài)。

          匿名函數(shù):Go語言中在一個(gè)函數(shù)前加go關(guān)鍵字就可以啟動協(xié)程,這個(gè)函數(shù)是可以沒有名字的(匿名)。在匿名函數(shù)之前定義的所有局部變量,在匿名函數(shù)中都是可見的。不幸的是,由于開發(fā)者可能不夠注意對這些在不同協(xié)程中的共享變量做保護(hù),從而可能容易導(dǎo)致數(shù)據(jù)競爭的bug。有11個(gè)bug就是這種類型,其中9個(gè)是父協(xié)程和子協(xié)程之間的數(shù)據(jù)競爭,2個(gè)是兩個(gè)子協(xié)程之間的數(shù)據(jù)競爭。如下圖的一個(gè)例子,含bug的版本中,變量i在父協(xié)程和子協(xié)程之間共享了,開發(fā)者想要得到不同的i值所生成的apiVersion,但是如果在父協(xié)程的for循環(huán)結(jié)束后子協(xié)程才運(yùn)行起來,那所有的apiVersion都將等于”v1.21”。解決方案就是將i作為參數(shù)傳遞到子協(xié)程中,此時(shí)傳遞的是i的拷貝。

          img

          WaitGroup的誤用:使用WaitGroup的一個(gè)基本準(zhǔn)則是,Add必須在Wait之前執(zhí)行。有6個(gè)bug是因?yàn)檫`反了這條準(zhǔn)則。如下圖所示,這是etcd中的一個(gè)bug,這里是無法保證func1中行8的Add一定在func2中行5的Wait之前執(zhí)行的。解決方案就是將Add操作遇到行6的位置,保證要么Add在Wait之前執(zhí)行,要么根本不會執(zhí)行到idle這個(gè)case。

          img

          特定庫函數(shù):go中有些類庫的變量是隱式在多協(xié)程中共享的。如context就被設(shè)計(jì)為可以被多個(gè)關(guān)聯(lián)協(xié)程訪問。etcd#7816就是因?yàn)樵诙鄠€(gè)協(xié)程中競爭使用一個(gè)context對象的一個(gè)字符串字段導(dǎo)致的。

          另一個(gè)例子是testing包。測試函數(shù)只有一個(gè)testing.T類型的變量,這個(gè)變量用于傳遞測試狀態(tài)如error何日志。有3個(gè)bug就是在測試函數(shù)以及測試函數(shù)內(nèi)啟動的子協(xié)程之間競爭使用testing.T變量導(dǎo)致。

          (2)消息傳遞中的錯(cuò)誤

          channel的誤用:前面也提到過,channel的使用需要遵循一定的規(guī)則,否則就會引起一些bug。如下圖所示(Docker#24007),可能有多個(gè)協(xié)程會運(yùn)行到這段代碼,其中可能有多個(gè)跑到了select的default分支,導(dǎo)致對channel的多次關(guān)閉,從而引發(fā)panic。這種情況,可以使用Once.Do將關(guān)閉channel的語句包起來,保證它只會執(zhí)行一次。

          img

          還有一種類型是將channel和select一起使用,當(dāng)select收到多個(gè)case的消息時(shí),是沒辦法保證會執(zhí)行哪一個(gè)的,這種非確定性的選擇,導(dǎo)致了3個(gè)bug。下圖是一個(gè)例子,其中f函數(shù)執(zhí)行耗時(shí)操作,當(dāng)它執(zhí)行完之后,stopCh的消息和ticker有可能同時(shí)到達(dá),此時(shí)并不一定會執(zhí)行到11行return語句,也有可能執(zhí)行到case <- ticker 這里,從而繼續(xù)循環(huán),f()沒必要地多執(zhí)行了一次。這種情況下,應(yīng)該在f()執(zhí)行的前后都判斷一下是否該退出循環(huán)。

          img

          特定庫函數(shù):一些庫函數(shù)內(nèi)部會使用channel,也可能導(dǎo)致非阻塞性bug。下圖是一個(gè)與time包有關(guān)的bug。開發(fā)者想實(shí)現(xiàn)的是,要么收到Done信號,要么超時(shí),然后再返回。但是含bug的版本先創(chuàng)建了超時(shí)時(shí)間為0的timer,然后再判斷參數(shù)dur是否大于0 ,大于0的話修改timer。但是,當(dāng)dur為0的情況下,timer實(shí)際上一開始就被設(shè)置為有信號了,可能導(dǎo)致函數(shù)過早返回。解決方案是不要讓timer過早創(chuàng)建。

          img

          非阻塞性bug的檢測

          Go提供了數(shù)據(jù)競爭檢測,在build的時(shí)候使用 -race 標(biāo)志即可啟用。

          文章的一些結(jié)論是,消息傳遞機(jī)制也容易造成bug,情況并不比共享內(nèi)存機(jī)制好。消息傳遞機(jī)制更多地會造成一些阻塞性bug,比較少造成非阻塞性bug,而且可以用于解決由于共享內(nèi)存導(dǎo)致的非阻塞性bug。

          關(guān)于bug檢測,目前很多在傳統(tǒng)語言中針對共享內(nèi)存的檢測算法,在go中也是適用的,但是針對go的消息傳遞機(jī)制所引起bug的檢測,還需研究。

          譯者:Darlzan

          譯文鏈接:https://blog.csdn.net/notjusttech/article/details/88294964



          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包(下圖只是部分),同時(shí)還包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。

          關(guān)注公眾號 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。


          瀏覽 44
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本国产视频 | 依依成人大香蕉 | 日韩无码首页 | 一级黄色片视频欧美 | 又黑又长的大黑鸡巴免费高清 |