? ? ? ?在上一篇文章中,我用goroutine和通道來實(shí)現(xiàn)了一種直接和自然的并發(fā)方式。在串行程序中(也就是一個(gè)程序只有一個(gè)goroutine),程序中各個(gè)步驟的執(zhí)行順序由程序邏輯來決定,比如在一系列語句中,第一句在第二句之前執(zhí)行,以此類推。如果沒有先后順序,那么這兩件事就是并發(fā)的。在并發(fā)調(diào)用的時(shí)候如果沒有額外的同步機(jī)制的情況下,從兩個(gè)或多個(gè)goroutine同時(shí)調(diào)用這個(gè)函數(shù),它們都能正常的運(yùn)行,那么我們稱之為這個(gè)函數(shù)是并發(fā)安全的。
? ? ? ?我們經(jīng)常會(huì)遇到并發(fā)調(diào)用不工作的情況,包括死鎖、活鎖以及資源耗盡,最重要的一個(gè)情形,叫做競(jìng)態(tài),競(jìng)態(tài)說的是多個(gè)goroutine按某些交錯(cuò)順序執(zhí)行時(shí),無法給出正確的結(jié)果,特別是高并發(fā)的時(shí)候出現(xiàn)的機(jī)率很大。競(jìng)態(tài)對(duì)于程序是致命的,因?yàn)樗鼈兛赡軡摲诔绦蛑校霈F(xiàn)頻率很低,有可能只會(huì)出現(xiàn)在高并發(fā)的情況下,這讓競(jìng)態(tài)很難去發(fā)現(xiàn)和分析。
? ? ? ?如何避免競(jìng)態(tài)?一共有三種方法,第一種方法是不要修改變量,第二種方法是避免從多個(gè)goroutine訪問同一個(gè)變量,第三種是允許多個(gè)goroutine訪問同一個(gè)變量,但同一時(shí)間只有一個(gè)goroutine可以訪問,這種方法稱為互斥機(jī)制。
? ? ? ?接下來講講互斥鎖這個(gè)重要的概念,在上一篇文章中,我們使用了緩沖通道實(shí)現(xiàn)了一個(gè)計(jì)數(shù)信號(hào)量,用于確認(rèn)同時(shí)發(fā)起HTTP請(qǐng)求的goroutine數(shù)量不超過20。使用同樣的理念,也可以用一個(gè)容量為1的通道來保證同一時(shí)間最多有一個(gè)goroutine能訪問共享變量?;コ怄i模式應(yīng)用非常廣泛,所以sync包有一個(gè)單獨(dú)的Mutex類型來支持這種模式。它的Lock方法用于獲取令牌token,而unlock方法是用來釋放令牌的。當(dāng)然了,要注意在Lock和Unlock之間的代碼,可以自由讀取和修改共享變量,這一部分稱為臨界區(qū)域。在鎖的持有人調(diào)用Unlock之前,其他goroutine不能獲取鎖。所以很重要的一點(diǎn)是,goroutine在使用之后應(yīng)該立即釋放,另外需要包含函數(shù)的所有分值,特別是錯(cuò)誤分支。這和什么比較像,有點(diǎn)像連接數(shù)據(jù)庫,在程序開始的時(shí)候進(jìn)行連接,使用完的時(shí)候需要及時(shí)關(guān)閉,不然會(huì)影響數(shù)據(jù)庫的性能。
? ? ? ?延遲是一個(gè)昂貴的初始化步驟到有實(shí)際需要的時(shí)刻是一個(gè)很好的實(shí)踐。預(yù)先初始化一個(gè)變量會(huì)增加程序的起動(dòng)延時(shí),并且如果實(shí)際執(zhí)行時(shí)間有可能根本用不上這個(gè)變量,那么初始化也不是必需的。在Go里面針對(duì)并發(fā)的問題,它設(shè)計(jì)了一個(gè)很好的機(jī)制,用于動(dòng)態(tài)分析,叫做競(jìng)態(tài)檢測(cè)器。用法是加上-race命令到go build、go run、go test命令中,這個(gè)方法會(huì)記錄下執(zhí)行對(duì)應(yīng)的共享變量的事件,包含go語句、通道操作、Lock調(diào)用、wait調(diào)用等等。要注意,這個(gè)競(jìng)態(tài)檢測(cè)器會(huì)消耗額外的執(zhí)行時(shí)間和內(nèi)存,所以僅僅建議使用在測(cè)試環(huán)境中,不建議使用在生產(chǎn)環(huán)境中,這可以幫助我們減少很多寶貴的調(diào)試時(shí)間。