您好,登錄后才能下訂單哦!
今天小編給大家分享一下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)鏈表
sendx
和recvx
用于記錄buf
這個(gè)循環(huán)鏈表中的~發(fā)送或者接收的~index
lock
是個(gè)互斥鎖。
recvq
和sendq
分別是接收(<-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è)指針。
先考慮一個(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è)資訊頻道。
免責(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)容。