Go開(kāi)發(fā)競(jìng)態(tài)檢測(cè)科普文
一、名詞解析
1、data race: Any race is a bug
定義: ①多個(gè)線程(協(xié)程)對(duì)于同一個(gè)變量、②同時(shí)地、③進(jìn)行讀/寫(xiě)操作、并且④至少有一個(gè)線程進(jìn)行寫(xiě)操作。(也就是說(shuō),如果所有線程都是只進(jìn)行讀操作,那么將不構(gòu)成數(shù)據(jù)爭(zhēng)用)
后果: 如果發(fā)生了數(shù)據(jù)爭(zhēng)用,讀取該變量時(shí)得到的值將變得不可知(根據(jù)內(nèi)存模型),使得該多線程程序的運(yùn)行結(jié)果將完全不可預(yù)測(cè),有一定可能會(huì)導(dǎo)致直接崩潰。
如何防止: 對(duì)于有可能被多個(gè)線程同時(shí)訪問(wèn)的變量使用排他訪問(wèn)控制,具體方法包括使用mutex(互斥量)或者使用atomic變量。
---------------------------------
作者注加一條規(guī)則:凡是若干線程(協(xié)程)對(duì)一個(gè)共享變量進(jìn)行同步操作,且其中有一個(gè)是寫(xiě)操作的,那么讀/寫(xiě)都要考慮使用原子操作。
---------------------------------
race condition(競(jìng)態(tài)條件)
讀取到數(shù)據(jù)中間狀態(tài)的的情形就是 race condition。相對(duì)于數(shù)據(jù)爭(zhēng)用(data race),競(jìng)態(tài)條件(race condition)指的是更加高層次的更加復(fù)雜的現(xiàn)象,一般需要在設(shè)計(jì)并行程序時(shí)進(jìn)行細(xì)致入微的分析,才能確定。(也就是隱藏得更深).
定義:受各線程上代碼執(zhí)行的順序和時(shí)機(jī)的影響,程序的運(yùn)行結(jié)果產(chǎn)生(預(yù)料之外)的變化。
后果:如果存在競(jìng)態(tài)條件(race condition),多次運(yùn)行程序?qū)τ谕粋€(gè)輸入將會(huì)有不同的結(jié)果,但結(jié)果并非完全不可預(yù)測(cè),它將由輸入數(shù)據(jù)和各線程的執(zhí)行順序共同決定。
如何預(yù)防:競(jìng)態(tài)條件產(chǎn)生的原因很多是對(duì)于同一個(gè)資源的一系列連續(xù)操作并不是原子性的,也就是說(shuō)有可能在執(zhí)行的中途被其他線程搶占,同時(shí)這個(gè)“其他線程”剛好也要訪問(wèn)這個(gè)資源。解決方法通常是:將這一系列操作作為一個(gè)critical section(臨界區(qū))。
2、undefined behavior(未定義行為)
未定義行為是指執(zhí)行某種計(jì)算機(jī)代碼所產(chǎn)生的結(jié)果,這種代碼在當(dāng)前程序狀態(tài)下的行為在其所使用的語(yǔ)言標(biāo)準(zhǔn)中沒(méi)有規(guī)定。在 Go 的內(nèi)存模型中,有race的Go程序的行為是未定義行為
3、go run/build -race(開(kāi)啟race檢測(cè))
golang在1.1之后引入了競(jìng)爭(zhēng)檢測(cè)的概念。我們可以使用go run -race或者go build -race來(lái)進(jìn)行競(jìng)爭(zhēng)檢測(cè)。-race選項(xiàng)打開(kāi)了data race detector用來(lái)檢查這個(gè)錯(cuò)誤,而且關(guān)閉了相關(guān)的編譯器優(yōu)化(作者注:就是為了不讓編譯器優(yōu)化代碼,能看清楚完整代碼)。(原因是)go編譯器認(rèn)為race代碼是dead code,可能直接優(yōu)化掉。
4、原子性
一個(gè)或者多個(gè)操作在 CPU 執(zhí)行的過(guò)程中不被中斷的特性,稱為原子性(atomicity)。這些操作對(duì)外表現(xiàn)成一個(gè)不可分割的整體,他們要么都執(zhí)行,要么都不執(zhí)行,外界不會(huì)看到他們只執(zhí)行到一半的狀態(tài)。
5、原子操作
原子操作(atomic operation)指的是由多步操作組成的一個(gè)操作。如果該操作不能原子地執(zhí)行,則要么執(zhí)行完所有步驟,要么一步也不執(zhí)行,不可能只執(zhí)行所有步驟的一個(gè)子集。
在單核系統(tǒng)里,單個(gè)的機(jī)器指令可以看成是原子操作(如果有編譯器優(yōu)化、亂序執(zhí)行等情況除外),在單核CPU中, 能夠在一個(gè)指令中完成的操作都可以看作為原子操作, 因?yàn)橹袛嘀话l(fā)生在指令間;
在多核系統(tǒng)中,單個(gè)的機(jī)器指令就不是原子操作,因?yàn)槎嗪讼到y(tǒng)里是多指令流并行運(yùn)行的,一個(gè)核在執(zhí)行一個(gè)指令時(shí),其他核同時(shí)執(zhí)行的指令有可能操作同一塊內(nèi)存區(qū)域,從而出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)現(xiàn)象。
多核系統(tǒng)中的原子操作通常使用內(nèi)存柵障(memory barrier)來(lái)實(shí)現(xiàn),即一個(gè)CPU核在執(zhí)行原子操作時(shí),其他CPU核必須停止對(duì)內(nèi)存操作或者不對(duì)指定的內(nèi)存進(jìn)行操作,這樣才能避免數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題。
在多核CPU的時(shí)代,體系中運(yùn)行著多個(gè)獨(dú)立的CPU,即使是可以在單個(gè)指令中完成的操作也可能會(huì)被干擾. 典型的例子就是decl指令(遞減指令), 它細(xì)分為三個(gè)過(guò)程: “讀->改->寫(xiě)”, 涉及兩次內(nèi)存操作。如果多個(gè)CPU運(yùn)行的多個(gè)進(jìn)程在同時(shí)對(duì)同一塊內(nèi)存執(zhí)行這個(gè)指令,那情況是無(wú)法預(yù)測(cè)的。
二、代碼分析
race 多協(xié)程寫(xiě)分析
golang在1.1之后引入了競(jìng)爭(zhēng)檢測(cè)的概念。我們可以使用go run -race 或者 go build -race 來(lái)進(jìn)行競(jìng)爭(zhēng)檢測(cè)。
golang語(yǔ)言內(nèi)部大概的實(shí)現(xiàn)就是同時(shí)開(kāi)啟多個(gè)goroutine執(zhí)行同一個(gè)命令,并且紀(jì)錄每個(gè)變量的狀態(tài)。
如果使用go run -race 1.go,將出現(xiàn)下列提示:
這個(gè)命令輸出了WARNING:DATA RACE
總結(jié)
本篇主要介紹了一些術(shù)語(yǔ),引用了一條規(guī)則:凡是多線程對(duì)共享變量涉及到寫(xiě)操作,都要考慮使用原子操作。
其次,介紹了-race選項(xiàng),可以對(duì)代碼涉及競(jìng)態(tài)的問(wèn)題做個(gè)檢查。