溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

golang的block和race怎么解決

發(fā)布時(shí)間:2022-01-15 17:55:23 來(lái)源:億速云 閱讀:127 作者:iii 欄目:大數(shù)據(jù)

今天小編給大家分享一下golang的block和race怎么解決的相關(guān)知識(shí)點(diǎn),內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識(shí),所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來(lái)了解一下吧。

一般并發(fā)的bug 有兩種,死鎖(block)和 競(jìng)爭(zhēng)(race)

死鎖發(fā)生時(shí),go run 會(huì)直接報(bào)錯(cuò)

race 發(fā)生時(shí),要加race 才會(huì)在運(yùn)行時(shí)報(bào)warning

go run xxx.go 后面加上 -race 參數(shù)

$ go run -race race.go

==================

WARNING: DATA RACE

Write at 0x00c0000a2000 by goroutine 6:

  main.main.func2()

      /Users/harryhare/git/go_playground/src/race.go:15 +0x38

Previous write at 0x00c0000a2000 by goroutine 5:

  main.main.func1()

      /Users/harryhare/git/go_playground/src/race.go:9 +0x38

Goroutine 6 (running) created at:

  main.main()

      /Users/harryhare/git/go_playground/src/race.go:13 +0x9c

Goroutine 5 (running) created at:

  main.main()

      /Users/harryhare/git/go_playground/src/race.go:7 +0x7a

package main

import "time"

func main(){

var x int

go func(){

for{

x=1

}

}()

go func(){

for{

x=2

}

}()

time.Sleep(100*time.Second)

}

這個(gè)命令輸出了Warning,告訴我們,goroutine5運(yùn)行到第11行和main goroutine運(yùn)行到13行的時(shí)候觸發(fā)競(jìng)爭(zhēng)了。

而且goroutine5是在第12行的時(shí)候產(chǎn)生的。

形成條件

一般情況下是由于在沒(méi)有加鎖的情況下多個(gè)協(xié)程進(jìn)行操作對(duì)同一個(gè)變量操作形成競(jìng)爭(zhēng)條件.

解決方式

方式1:使用互斥鎖sync.Mutex

方式2:使用管道

使用管道的效率要比互斥鎖高,也符合Go語(yǔ)言的設(shè)計(jì)思想.

在寫(xiě)如果檢測(cè)race之前,首先明白第一個(gè)問(wèn)題,什么是race?

當(dāng)多個(gè)goroutine同時(shí)在對(duì)同一個(gè)變量執(zhí)行讀和寫(xiě)沖突的操作時(shí),結(jié)果是不能確定的,這就是race。比如goroutine1在讀a,goroutine2在寫(xiě)a,如果不能確定goroutine1讀到的結(jié)果是goroutine2寫(xiě)之前還是寫(xiě)之后的值,就是race了。

var x int

go func() {

    v := x

}()

x = 5

上面的代碼v的值到底是0,還是5呢?不知道,這段代碼存在race。這是比較口頭的描述,嚴(yán)謹(jǐn)?shù)男问交拿枋?,就需要講Go的內(nèi)存模型。

Go的內(nèi)存模型描述的是"在一個(gè)groutine中對(duì)變量進(jìn)行讀操作能夠偵測(cè)到在其他goroutine中對(duì)該變量的寫(xiě)操作"的條件。

假設(shè)A和B表示一個(gè)多線程的程序執(zhí)行的兩個(gè)操作。如果A happens-before B,那么A操作對(duì)內(nèi)存的影響 將對(duì)執(zhí)行B的線程(且執(zhí)行B之前)可見(jiàn)。

有了happens before這么形式化的描述之后,是否有race,等價(jià)于對(duì)于同一塊內(nèi)存訪問(wèn),是否有存在無(wú)法判斷happens before的沖突操作。即是說(shuō):

對(duì)于前面那段代碼,v := x和x = 5兩個(gè)操作訪問(wèn)了同一塊內(nèi)存x,并且沒(méi)有任何保證v := x是happens before x = 5的,所以這段代碼有race。

那么"實(shí)現(xiàn)race dectect"這個(gè)問(wèn)題,就轉(zhuǎn)化成了"happens before事件的檢測(cè)問(wèn)題"。

如何檢測(cè)到happens before事件呢?

我們可以把"哪個(gè)線程id,在什么時(shí)間,訪問(wèn)哪塊內(nèi)存,是讀還是寫(xiě)",只要把所有內(nèi)存訪問(wèn)的事件都記錄下來(lái),然后遍歷,驗(yàn)證這些操作之間的先后順序。一旦發(fā)現(xiàn),比如,讀和寫(xiě)兩條操作記錄,無(wú)法滿足讀happens before寫(xiě),就是檢測(cè)到race了。

但是要記錄所有的內(nèi)存訪問(wèn)操作,看起來(lái)代價(jià)似乎有點(diǎn)嚇人。其實(shí)只是記錄可能會(huì)被并發(fā)訪問(wèn)的變量,并不是所有變量,下里的g是局部變量,就不需要記錄了。

func f() {

    g := 3

}

但是代價(jià)似乎還是很大?確實(shí)。好吧,會(huì)慢10倍還是100倍我不確定,反正線上代碼是不會(huì)開(kāi)race跑的。既然Go都已經(jīng)做了,肯定是能做的。

需要有兩部分,在Go里面-race編譯選項(xiàng)會(huì)做相應(yīng)的處理。編譯部分需要在涉及到內(nèi)存訪問(wèn)的地方插入指令來(lái)記錄事件;運(yùn)行時(shí)則是檢測(cè)事件之間的happens before。

一條內(nèi)存訪問(wèn)事件可以用8個(gè)字節(jié)來(lái)記錄:16位線程id,42位時(shí)間戳,5位記內(nèi)存位置,1位標(biāo)記是讀還是寫(xiě)。

線程id不用解釋,讀寫(xiě)標(biāo)記也不用解釋。時(shí)間戳是邏輯時(shí)鐘,不是每次取真實(shí)時(shí)間。

只用5位如何記錄內(nèi)存位置呢?這里就有點(diǎn)技巧了,Go的內(nèi)存管理也用到了同樣的技巧。對(duì)于實(shí)際使用的一塊內(nèi)存區(qū)域,映射另一塊"影子"內(nèi)存區(qū)域,映射出來(lái)的是真實(shí)的"影子"。

比如有一個(gè)數(shù)組A[1000],它的"影子"是B[1000]。A[i]里面發(fā)生了什么事件,只在記錄在B[i]里面就行了。注意兩者大小不需要是一樣的,比如

int  A[1000];   // 真實(shí)使用的數(shù)組

char B[1000];   // 用于記錄發(fā)生在A數(shù)組里面操作,如果只記讀/寫(xiě)1位足已,記其它也不一定用到8位

同理,對(duì)于實(shí)際使用的內(nèi)存區(qū)域是[0x7fffffffffff 0x7f0000000000],它的"影子"區(qū)域可以是[0x1fffffffffff 0x180000000000],5位可以表示64個(gè)單元,如果實(shí)際使用的內(nèi)存使用按8字節(jié)對(duì)齊之后,是足夠表示一組的。

好像有點(diǎn)說(shuō)不明白,這么解釋吧:3位可以表示8個(gè)單元的狀態(tài),對(duì)吧?2的3次方等于8

A[8個(gè)8字節(jié)的單元] => B[3位]

A里面是否發(fā)生了讀或者寫(xiě)的操作,在B里面用位的0或1記錄來(lái)下。說(shuō)明只用少量?jī)?nèi)存就可以記錄大量事件!

回到事件的記錄格式,一條記錄占8個(gè)字節(jié),其中有5位記錄內(nèi)存位置。5位是可以記錄64個(gè)8字節(jié)的,也就是race dectect的空間開(kāi)銷是使用的內(nèi)存的1/8(其實(shí)不是,因?yàn)閷?duì)同一內(nèi)存的事件,要記錄一組)。

看個(gè)例子,我們記錄下了第一條事件,線程T1,在E1時(shí)間戳,訪問(wèn)內(nèi)存區(qū)域[0 2],執(zhí)行寫(xiě)操作:

(T1,E1,0:2,W)

第二條事件,線程T2,在E2時(shí)間戳,讀內(nèi)存區(qū)域[4 8]:

(T2,E2,4:8,R)

因?yàn)槲恢脹](méi)有交集,所以沒(méi)有沖突。

第三條事件,線程T3,在E3時(shí)間戳,讀內(nèi)存區(qū)域[0 4]:

(T3,E3,0:4,R)

這個(gè)區(qū)域是跟第一個(gè)事件的區(qū)域有交集的,那么假設(shè)E1無(wú)法滿足happens before E3,那么就檢測(cè)到?jīng)_突了。

type hchan struct {
   qcount   uint           // total data in the queue 當(dāng)前隊(duì)列中的數(shù)據(jù)的個(gè)數(shù)
   dataqsiz uint           // size of the circular queue   channel環(huán)形隊(duì)列的大小
   buf      unsafe.Pointer // points to an array of dataqsiz elements  存放數(shù)據(jù)的環(huán)形隊(duì)列的指針
   elemsize uint16     // channel 中存放的數(shù)據(jù)類型的大小|即每個(gè)元素的大小
   closed   uint32     // channel 是否關(guān)閉的標(biāo)示
   elemtype *_type // element type channel中存放的元素的類型
   sendx    uint   // send index   當(dāng)前發(fā)送元素指向channel環(huán)形隊(duì)列的下標(biāo)指針
   recvx    uint   // receive index 當(dāng)前接收元素指向channel環(huán)形隊(duì)列的下標(biāo)指針
   recvq    waitq  // list of recv waiters 等待接收元素的goroutine隊(duì)列
   sendq    waitq  // list of send waiters  等待發(fā)送元素的goroutine隊(duì)列

   // lock protects all fields in hchan, as well as several
   // fields in sudogs blocked on this channel.
   //
   // Do not change another G's status while holding this lock
   // (in particular, do not ready a G), as this can deadlock
   // with stack shrinking.
   // 保持此鎖定時(shí)不要更改另一個(gè)G的狀態(tài)(特別是,沒(méi)有準(zhǔn)備好G),因?yàn)檫@可能會(huì)因堆棧收縮而死鎖。
   lock mutex
}

簡(jiǎn)單說(shuō)明:

  • buf是有緩沖的channel所特有的結(jié)構(gòu),用來(lái)存儲(chǔ)緩存數(shù)據(jù)。是個(gè)循環(huán)鏈表

  • sendxrecvx用于記錄buf這個(gè)循環(huán)鏈表中的~發(fā)送或者接收的~index

  • lock是個(gè)互斥鎖。

  • recvqsendq分別是接收(<-channel)或者發(fā)送(channel <- xxx)的goroutine抽象出來(lái)的結(jié)構(gòu)體(sudog)的隊(duì)列。是個(gè)雙向鏈表

源碼位于/runtime/chan.go中(目前版本:1.11)。

創(chuàng)建channel實(shí)際上就是在內(nèi)存中實(shí)例化了一個(gè)hchan的結(jié)構(gòu)體,并返回一個(gè)ch指針,我們使用過(guò)程中channel在函數(shù)之間的傳遞都是用的這個(gè)指針,這就是為什么函數(shù)傳遞中無(wú)需使用channel的指針,而直接用channel就行了,因?yàn)閏hannel本身就是一個(gè)指針。

channel中發(fā)送send(ch <- xxx)和recv(<- ch)接收

先考慮一個(gè)問(wèn)題,如果你想讓goroutine以先進(jìn)先出(FIFO)的方式進(jìn)入一個(gè)結(jié)構(gòu)體中,你會(huì)怎么操作?加鎖!對(duì)的!channel就是用了一個(gè)鎖。hchan本身包含一個(gè)互斥鎖mutex

以上就是“golang的block和race怎么解決”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會(huì)為大家更新不同的知識(shí),如果還想學(xué)習(xí)更多的知識(shí),請(qǐng)關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI