Go語(yǔ)言中的數(shù)據(jù)競(jìng)爭(zhēng)模式詳解
前言
本文主要基于在Uber的Go monorepo中發(fā)現(xiàn)的各種數(shù)據(jù)競(jìng)爭(zhēng)模式,分析了其背后的原因與分類,希望能夠幫助更多的Go開發(fā)人員,去關(guān)注并發(fā)代碼的編寫,考慮不同的語(yǔ)言的特性、以及避免由于自身編程習(xí)慣所引發(fā)的并發(fā)錯(cuò)誤。
近年來(lái),Uber已經(jīng)開始采用Golang(簡(jiǎn)稱Go)作為開發(fā)微服務(wù)的主要編程語(yǔ)言。目前,其Go monorepo(譯者注:包含多個(gè)不同項(xiàng)目的單個(gè)倉(cāng)庫(kù))包含了大約5,000萬(wàn)行代碼,以及大約2,100個(gè)獨(dú)特的Go服務(wù)。而且,它們都還在持續(xù)增長(zhǎng)中。
為了實(shí)現(xiàn)并發(fā),我們通常會(huì)使用go關(guān)鍵字,為函數(shù)調(diào)用添加前綴,以實(shí)現(xiàn)異步式的運(yùn)行調(diào)用。在Go中,此類異步函數(shù)調(diào)用被稱為goroutine。開發(fā)人員可以通過(guò)創(chuàng)建goroutine(例如,對(duì)其他服務(wù)的IO或RPC調(diào)用),來(lái)隱藏延遲。不同的goroutine可以通過(guò)消息傳遞,以及共享內(nèi)存的方式,來(lái)傳遞數(shù)據(jù)。其中,共享內(nèi)存恰好是Go中最常用的數(shù)據(jù)通信方式之一。
由于goroutineGo很容易被程序員創(chuàng)建和使用,因此它被認(rèn)為屬于“輕量級(jí)” 。同時(shí),由Go編寫的程序通常會(huì)比由其他語(yǔ)言編寫的程序具有更強(qiáng)的并發(fā)性。例如,通過(guò)掃描數(shù)十萬(wàn)個(gè)運(yùn)行在數(shù)據(jù)中心的微服務(wù)實(shí)例,我們發(fā)現(xiàn)Go微服務(wù)的并發(fā)性可達(dá)Java微服務(wù)的8倍。
當(dāng)然,更高的并發(fā)性也意味著更多潛在的并發(fā)錯(cuò)誤。我們常用數(shù)據(jù)競(jìng)爭(zhēng)(data race)來(lái)描述當(dāng)兩個(gè)或多個(gè)goroutine訪問(wèn)相同的數(shù)據(jù),而且至少有一個(gè)處于寫入狀態(tài)時(shí),由于它們之間并沒(méi)有排序,因此就會(huì)發(fā)生并發(fā)錯(cuò)誤??偟膩?lái)說(shuō),根據(jù)Go自身的相互作用等特點(diǎn),數(shù)據(jù)競(jìng)爭(zhēng)之類的隱蔽錯(cuò)誤非常容易出現(xiàn),因此我們應(yīng)該盡量避免。
最近,我們使用動(dòng)態(tài)數(shù)據(jù)競(jìng)爭(zhēng)檢測(cè)技術(shù)開發(fā)了一個(gè)系統(tǒng),專門用來(lái)檢測(cè)Uber的數(shù)據(jù)競(jìng)爭(zhēng)。它在上線的六個(gè)月時(shí)間內(nèi),在我們的Go代碼庫(kù)中,檢測(cè)到了大約2,000個(gè)數(shù)據(jù)競(jìng)爭(zhēng)。其中已被開發(fā)人員著手修復(fù)了的數(shù)據(jù)競(jìng)爭(zhēng)約有1,100個(gè)。下面,我將向您展示我們已發(fā)現(xiàn)的各種常見(jiàn)數(shù)據(jù)競(jìng)爭(zhēng)模式。
Go在goroutine中通過(guò)引用來(lái)透明地捕獲自由變量
Go中的嵌套函數(shù)(又名closure)通過(guò)引用的方式,透明地捕獲所有自由的變量。程序員通常無(wú)需明確指定在closure語(yǔ)法中,需要捕獲哪些自由變量。
這種方式是有別于Java和C++的。Java的lambda僅會(huì)根據(jù)數(shù)值去捕獲,而且他們會(huì)有意識(shí)地避免并發(fā)缺陷。而C++則要求開發(fā)人員明確地指明是使用數(shù)值、還是引用的捕獲方式。
當(dāng)closure較大時(shí),開發(fā)人員并不知道closure內(nèi)使用的變量是否自由,可否通過(guò)引用來(lái)捕獲。而由于引用的捕獲、以及goroutine都是并發(fā)的,因此Go程序最終可能會(huì)因?yàn)闆](méi)能顯式地執(zhí)行同步,而對(duì)自由變量進(jìn)行無(wú)序的訪問(wèn)。我們可以通過(guò)如下三個(gè)示例來(lái)證明這一點(diǎn):
示例1:由循環(huán)索引的變量捕獲,而導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)
圖1A中的代碼顯示了迭代Go的切片作業(yè),并通過(guò)ProcessJob函數(shù)來(lái)處理每個(gè)元素的作業(yè)。

圖1A:由循環(huán)索引的變量捕獲,而導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)。
在此,開發(fā)人員會(huì)將厚重的ProcessJob包裝在一個(gè)匿名的goroutine中。但是,循環(huán)索引變量的作業(yè)是通過(guò)goroutine內(nèi)部被引用捕獲的。當(dāng)goroutine為首次循環(huán)迭代而啟動(dòng),并訪問(wèn)作業(yè)的變量時(shí),父goroutine中的for循環(huán)將在切片中更新相同的循環(huán)索引變量作業(yè),并指向切片中的第二個(gè)元素,這就會(huì)導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)的出現(xiàn)。此類數(shù)據(jù)競(jìng)爭(zhēng)可能發(fā)生在數(shù)值和引用類型上;切片、數(shù)組和映射上;以及循環(huán)體中的讀和寫的訪問(wèn)中。為此,Go推薦了一種編碼習(xí)慣,來(lái)隱藏和私有化循環(huán)體中循環(huán)索引的變量。不過(guò),開發(fā)人員并不總是能夠遵循這一點(diǎn)。
示例2:由err變量的捕獲,所導(dǎo)致的數(shù)據(jù)競(jìng)爭(zhēng)

圖1B:由err變量的捕獲,所導(dǎo)致的數(shù)據(jù)競(jìng)爭(zhēng)。
Go一直提倡函數(shù)有多個(gè)返回值。圖1B展示了一種常見(jiàn)的通過(guò)返回實(shí)際值和錯(cuò)誤對(duì)象,來(lái)指示是否存在錯(cuò)誤的用法??梢?jiàn),當(dāng)且僅當(dāng)錯(cuò)誤值為nil(空)時(shí),實(shí)際的返回值才會(huì)被認(rèn)為是有意義的。因此,我們的通常做法是:將返回的錯(cuò)誤對(duì)象,分配給名為err的變量,然后檢查其是否為空(nilness)。不過(guò),由于我們可以在函數(shù)體內(nèi)調(diào)用多個(gè)返回錯(cuò)誤的函數(shù),因此程序每次都會(huì)對(duì)err變量進(jìn)行多次賦值,然后進(jìn)行是否為空的檢查。當(dāng)開發(fā)人員將這個(gè)習(xí)慣用法與goroutine混合使用時(shí),錯(cuò)誤變量就會(huì)在closure中被引用捕獲。結(jié)果,程序?qū)τ趃oroutine中err的讀寫訪問(wèn),與隨后對(duì)封閉函數(shù)(或goroutine的多個(gè)實(shí)例)中相同的err變量的讀寫操作,就會(huì)同時(shí)運(yùn)行。這便導(dǎo)致了數(shù)據(jù)競(jìng)爭(zhēng)。
示例3:由已命名的返回變量捕獲,所導(dǎo)致的數(shù)據(jù)競(jìng)爭(zhēng)

圖1C:由已命名的返回變量捕獲,所導(dǎo)致的數(shù)據(jù)競(jìng)爭(zhēng)。
Go引入了一種被稱為已命名返回值的語(yǔ)法塊。已命名的返回變量被視為在函數(shù)頂部定義的變量,其作用域超出了函數(shù)體。而沒(méi)有參數(shù)的return語(yǔ)句,被稱為“裸”命名返回值。由于closure的存在,如果將正常(非裸)的返回與已命名的返回相混合、或在具有命名返回的函數(shù)中使用延遲返回,那么就可能會(huì)引發(fā)數(shù)據(jù)競(jìng)爭(zhēng)。
在上圖1C中的NamedReturnCallee函數(shù)返回了一個(gè)整數(shù),而且返回變量被命名為result。根據(jù)該語(yǔ)法,函數(shù)體的其余部分可以對(duì)結(jié)果進(jìn)行直接讀寫,而無(wú)需額外聲明。如果函數(shù)在第4行返回的是一個(gè)裸返回,而由于在第2行被賦值為result=10,那么第13行的調(diào)用者將看到其返回值為10。編譯器則會(huì)安排將結(jié)果復(fù)制到retVal。同時(shí),已命名的返回函數(shù)也可以使用如第9行所示的標(biāo)準(zhǔn)返回語(yǔ)法。該語(yǔ)法會(huì)讓編譯器復(fù)制return語(yǔ)句中的返回值20,以分配給已命名的返回變量結(jié)果。第6行創(chuàng)建了一個(gè)goroutine,它會(huì)捕獲已命名的返回變量的結(jié)果。在設(shè)置該goroutine時(shí),即使是并發(fā)專家也可能認(rèn)為讀取第7行的結(jié)果中是安全的,畢竟不存在對(duì)同一變量的寫入,而且第9行的語(yǔ)句返回的20是一個(gè)常量,它似乎并沒(méi)有觸及到已命名的返回變量結(jié)果。不過(guò),如前所述,代碼在生成的過(guò)程中,會(huì)將return 20的語(yǔ)句轉(zhuǎn)換為寫入結(jié)果。此時(shí),一旦我們突然對(duì)共享的結(jié)果變量進(jìn)行并發(fā)讀寫,就會(huì)產(chǎn)生數(shù)據(jù)競(jìng)爭(zhēng)的情況。
切片會(huì)產(chǎn)生難以診斷的數(shù)據(jù)競(jìng)爭(zhēng)
切片(Slices)實(shí)際上是一些動(dòng)態(tài)數(shù)組和引用類型。在其內(nèi)部,切片包含了一個(gè)指向底層數(shù)組的指針、它的當(dāng)前長(zhǎng)度、以及底層數(shù)組可以擴(kuò)展的最大容量。為了便于討論,我們將這些變量統(tǒng)稱為切片的元字段(meta field)。切片上的一種常見(jiàn)操作便是通過(guò)追加操作(append operation)來(lái)使其增長(zhǎng)。當(dāng)達(dá)到其容量限制時(shí),代碼會(huì)進(jìn)行新的分配(例如,對(duì)當(dāng)前的容量翻倍),并更新其對(duì)應(yīng)的元字段。而當(dāng)一個(gè)切片被goroutine并發(fā)訪問(wèn)時(shí),Go會(huì)通過(guò)互斥鎖(mutex),來(lái)保護(hù)對(duì)它的訪問(wèn)。

圖2:即使使用鎖,切片仍會(huì)出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)。
在圖2中,開發(fā)人員往往以為已經(jīng)對(duì)第6行的切片進(jìn)行了鎖定保護(hù),便可防止數(shù)據(jù)競(jìng)爭(zhēng)的出現(xiàn)。而實(shí)際上,當(dāng)?shù)?4行將切片作為參數(shù)傳遞給沒(méi)有鎖保護(hù)的goroutine時(shí),就會(huì)產(chǎn)生數(shù)據(jù)競(jìng)爭(zhēng)。具體而言,goroutine的調(diào)用導(dǎo)致了切片中的元字段從調(diào)用處(第14行)被復(fù)制到被調(diào)用者(第11行)處??紤]到切片屬于引用類型,我們認(rèn)為在將其傳遞(復(fù)制)到被調(diào)用者時(shí),會(huì)導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)的發(fā)生。不過(guò),由于切片與指針類型不同,畢竟元字段是按照數(shù)值復(fù)制的,因此該數(shù)據(jù)競(jìng)爭(zhēng)的發(fā)生概率非常低。
并發(fā)訪問(wèn)Go內(nèi)置的、不安全的線程映射會(huì)導(dǎo)致頻繁的數(shù)據(jù)競(jìng)爭(zhēng)
哈希表(或稱映射)是Go中的內(nèi)置語(yǔ)言功能。不過(guò),它對(duì)于線程是不安全的。如果多個(gè)goroutine同時(shí)訪問(wèn)同一張哈希表,而且其中至少有一個(gè)試圖去修改哈希表(插入或刪除某項(xiàng))的話,就會(huì)產(chǎn)生數(shù)據(jù)競(jìng)爭(zhēng)。開發(fā)人員往往認(rèn)為他們可以同時(shí)訪問(wèn)哈希表中的不同項(xiàng)。而實(shí)際上,與數(shù)組或切片不同,映射(哈希表)是一種稀疏的數(shù)據(jù)結(jié)構(gòu),訪問(wèn)某一個(gè)元素就可能會(huì)導(dǎo)致訪問(wèn)另一個(gè)元素,如果在同一過(guò)程中發(fā)生了另一種插入或刪除,那么它將會(huì)因?yàn)樾薷牧讼∈璧臄?shù)據(jù)結(jié)構(gòu),而導(dǎo)致了數(shù)據(jù)競(jìng)爭(zhēng)。
我們甚至觀察到了更為復(fù)雜的、由并發(fā)映射訪問(wèn)產(chǎn)生的數(shù)據(jù)競(jìng)爭(zhēng)。其原因是同一個(gè)哈希表被傳遞到了深度調(diào)用路徑,而開發(fā)人員忘記了這些調(diào)用路徑是通過(guò)異步goroutine去改變哈希表的事實(shí)。圖3便顯示了此類數(shù)據(jù)競(jìng)爭(zhēng)的示例。

圖3:由于并發(fā)映射訪問(wèn)導(dǎo)致的數(shù)據(jù)競(jìng)爭(zhēng)。
雖然導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)的哈希表并非Go獨(dú)有,但是以下原因會(huì)讓Go更容易發(fā)生數(shù)據(jù)競(jìng)爭(zhēng):
- 由于映射是一種內(nèi)置的語(yǔ)言結(jié)構(gòu),因此Go開發(fā)人員會(huì)比其他語(yǔ)言的開發(fā)者更頻繁地使用映射。例如,在我們的Java存儲(chǔ)庫(kù)中,每MLoC(Millions of Lines Of Code,數(shù)百萬(wàn)行代碼)里有4,389個(gè)映射結(jié)構(gòu);而在Go中,每MLoC里就有5,950個(gè)映射,足足高出了1.34倍。
- 不同于Java的get和put API,哈希表的訪問(wèn)語(yǔ)法類似數(shù)組訪問(wèn)語(yǔ)法,雖然易于使用,但是也會(huì)意外地與隨機(jī)訪問(wèn)數(shù)據(jù)結(jié)構(gòu)相混淆。在Go中,我們可以使用table[key]的語(yǔ)法,輕松查詢那些不存在(non-existing)的映射元素。該語(yǔ)法能夠簡(jiǎn)單地返回默認(rèn)值,而不會(huì)產(chǎn)生任何錯(cuò)誤。這種容錯(cuò)性對(duì)于開發(fā)者在使用Go的映射時(shí)是非常友好的。
Go開發(fā)人員常在pass-by-value時(shí)犯錯(cuò)并導(dǎo)致non-trivial的數(shù)據(jù)競(jìng)爭(zhēng)
Go建議使用pass-by-value的語(yǔ)義,以簡(jiǎn)化逃逸分析,并為變量提供更好的棧上分配的機(jī)會(huì),進(jìn)而減少垃圾收集器的壓力。
與所有對(duì)象皆為引用類型的Java不同,在Go中,對(duì)象可以是數(shù)值類型(如:結(jié)構(gòu)),也可以是引用類型(如:接口)。由于沒(méi)有了語(yǔ)法差異,這會(huì)導(dǎo)致諸如:sync.Mutex和sync.RWMutex等數(shù)值類型,在同步構(gòu)造中被錯(cuò)誤地使用。如果一個(gè)函數(shù)創(chuàng)建了一個(gè)互斥體結(jié)構(gòu),并通過(guò)數(shù)值傳遞(pass-by-value)給多個(gè)goroutine調(diào)用,那么這些goroutine在并發(fā)執(zhí)行時(shí),不同的互斥對(duì)象是不會(huì)在操作過(guò)程中共享內(nèi)部狀態(tài)的。這也就破壞了對(duì)于受保護(hù)的共享內(nèi)存區(qū)域的互斥訪問(wèn)特性。請(qǐng)參見(jiàn)如下圖4所示的代碼。

圖4A:
由by-reference或by-pointer的方法調(diào)用所引起的數(shù)據(jù)競(jìng)爭(zhēng)

圖4B:sync.Mutex的Lock/Unlock簽名。
由于Go語(yǔ)法在指針和數(shù)值上調(diào)用方法是相同的,因此開發(fā)人員往往會(huì)忽視m.Lock()正在處理互斥鎖的副本并非指針這一問(wèn)題。調(diào)用者仍然可以在互斥的數(shù)值上調(diào)用這些API。而且編譯器也會(huì)透明地安排傳遞數(shù)值的地址。相反,如果沒(méi)有此類透明度,該錯(cuò)誤就能夠會(huì)被檢測(cè)到,并認(rèn)定為編譯器類型不匹配的錯(cuò)誤。
據(jù)此,當(dāng)開發(fā)人員意外地實(shí)現(xiàn)了一個(gè)方法,其中的接收者是指向結(jié)構(gòu)的指針,而不是結(jié)構(gòu)的數(shù)值或副本時(shí),那么就會(huì)發(fā)生與此相反的情況。也就是說(shuō),調(diào)用該方法的多個(gè)goroutine,最終會(huì)意外地共享結(jié)構(gòu)相同的內(nèi)部狀態(tài)。而且,調(diào)用者也不會(huì)意識(shí)到數(shù)值類型在接收者處被透明地轉(zhuǎn)換為了指針類型。顯然,這都是開發(fā)人員所不愿發(fā)生的。
消息傳遞(通道)和共享內(nèi)存的混合使用使代碼變得復(fù)雜且易受數(shù)據(jù)競(jìng)爭(zhēng)的影響

圖5:將消息傳遞與共享內(nèi)存混合時(shí)的數(shù)據(jù)競(jìng)爭(zhēng)。
圖5展示了開發(fā)人員使用一個(gè)專門為信號(hào)和等待準(zhǔn)備的通道,通過(guò)Future來(lái)實(shí)現(xiàn)的示例。我們可以通過(guò)調(diào)用Start()方法來(lái)啟動(dòng)Future,并通過(guò)調(diào)用Future的Wait()方法,來(lái)阻止Future的完成。Start()方法會(huì)創(chuàng)建一個(gè)goroutine,以執(zhí)行一個(gè)注冊(cè)到Future的函數(shù),并記錄其返回值(如:response和err)。如第6行所示,goroutine通過(guò)在通道ch上發(fā)送一條消息,以向Wait()方法發(fā)出Future完成的信號(hào)。對(duì)稱地,如第11行所示,Wait()方法塊會(huì)從通道中獲取相應(yīng)的消息。
在Go中,上下文攜帶了跨越API邊界和進(jìn)程之間的截止日期、取消信號(hào)和其他請(qǐng)求范圍的數(shù)值。這是在微服務(wù)中為任務(wù)設(shè)置時(shí)間線的常見(jiàn)模式。由此,Wait()阻止了被取消(第13行)的上下文、或已完成的Future(第11行)。此外,Wait()被包裝在一個(gè)select語(yǔ)句(第10行)中,并處于阻止?fàn)顟B(tài),直到至少有一個(gè)選擇arm準(zhǔn)備就緒。
如果上下文超時(shí),則相應(yīng)的案例將Future的err字段,在第14行上記錄為ErrCancelled。此時(shí),對(duì)于err的寫入與第5行對(duì)Future的相同變量的寫入操作,便形成了競(jìng)爭(zhēng)。
Add和Done方法的錯(cuò)誤放置會(huì)導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)
sync.WaitGroup結(jié)構(gòu)是Go的組同步結(jié)構(gòu)。與C++的barrier的barrier、以及l(fā)atch的構(gòu)造不同,WaitGroup中參與者的數(shù)量不是在構(gòu)造時(shí)被確定的,而是動(dòng)態(tài)更新的。在WaitGroup對(duì)象上,Go允許進(jìn)行Add(int)、Done()和Wait()三種操作。其中,Add()會(huì)增加參與者的計(jì)數(shù),而Wait()會(huì)處于阻止?fàn)顟B(tài),直到Done()被調(diào)用為count的次數(shù)(通常每個(gè)參與者一次)。由于在Go中,組同步的使用程度比Java高出1.9倍,因此WaitGroup在Go中常被廣泛地使用。
在下圖6中,開發(fā)人員打算創(chuàng)建與切片itemId里的元素?cái)?shù)量相同的goroutine,且并發(fā)處理它們。每個(gè)goroutine在不同索引的結(jié)果切片、以及在第12行對(duì)父功能塊中,記錄其成功或失敗的狀態(tài),直到所有的goroutine已完成。接著,它會(huì)依次訪問(wèn)結(jié)果中的所有元素,以計(jì)算出被成功處理的數(shù)量。

圖6A:
由于WaitGroup.Add()的錯(cuò)誤放置,導(dǎo)致了數(shù)據(jù)競(jìng)爭(zhēng)
為了使該代碼能夠正常工作,我們需要在第12行調(diào)用Wait()時(shí),保證wg.Add(1)在調(diào)用wg.Wait()之前所執(zhí)行的次數(shù),也就是注冊(cè)參與者的數(shù)量,必須等于itemIds的長(zhǎng)度。這就意味著wg.Add(1)應(yīng)該在每個(gè)goroutine之前被放置在第5行調(diào)用。但是,如果開發(fā)人員在第7行錯(cuò)誤地將wg.Add(1)放置在了goroutine的主體中,它就無(wú)法保證在外部函數(shù)WaitGrpExample調(diào)用Wait()時(shí),完整地執(zhí)行。據(jù)此,在調(diào)用Wait()時(shí),被注冊(cè)到WaitGroup的itemId的長(zhǎng)度就可能會(huì)變短。正是出于該原因,Wait()會(huì)被提前解除阻止。據(jù)此,WaitGrpExample函數(shù)則可以從切片結(jié)果中開始讀取(即:第13行),而一些goroutine則開始并發(fā)寫入同一個(gè)切片。
此外,我們還發(fā)現(xiàn)過(guò)早地在Waitgroup上調(diào)用wg.Done(),也會(huì)導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)。下圖6B展示了wg.Done()與Go的defer語(yǔ)句交互的結(jié)果。當(dāng)遇到多個(gè)defer語(yǔ)句時(shí),代碼會(huì)按照“后進(jìn)先出”的順序去執(zhí)行。其中,第9行的wg.Wait()會(huì)在doCleanup()運(yùn)行之前完成。即,父goroutine會(huì)在第10行去訪問(wèn)locationErr,而子goroutine可能仍然在延遲的doCleanup()函數(shù)內(nèi)寫入locationErr(為簡(jiǎn)潔起見(jiàn),在此并未顯示)。

圖6B:由于WaitGroup.Done()的錯(cuò)誤放置
延遲語(yǔ)句排序,并導(dǎo)致了數(shù)據(jù)競(jìng)爭(zhēng)。
并發(fā)運(yùn)行測(cè)試會(huì)導(dǎo)致產(chǎn)品或測(cè)試代碼中的數(shù)據(jù)競(jìng)爭(zhēng)
測(cè)試是Go的內(nèi)置功能。在那些后綴為_test.go的文件里,任何前綴為Test的函數(shù),都可以測(cè)試由Go構(gòu)建的系統(tǒng)。如果測(cè)試代碼調(diào)用了API--testing.T.Parallel(),那么它將與其他同類測(cè)試并發(fā)運(yùn)行。我們發(fā)現(xiàn)此類并發(fā)測(cè)試有時(shí)會(huì)在測(cè)試代碼中、有時(shí)也會(huì)在產(chǎn)品代碼中產(chǎn)生大量的數(shù)據(jù)競(jìng)爭(zhēng)。
此外,在單個(gè)以Test為前綴的函數(shù)中,Go開發(fā)人員經(jīng)常會(huì)編寫許多子測(cè)試,并通過(guò)由Go提供的套件包去執(zhí)行它們。Go推薦開發(fā)人員通過(guò)表驅(qū)動(dòng)的測(cè)試套件習(xí)語(yǔ)(table-driven test suite idiom)去編寫和運(yùn)行測(cè)試套件。據(jù)此,我們的開發(fā)人員在同一個(gè)測(cè)試中就編寫了數(shù)十、甚至數(shù)百個(gè)可供系統(tǒng)并發(fā)運(yùn)行的子測(cè)試。開發(fā)人員以為代碼會(huì)執(zhí)行串行測(cè)試,而忘記了在大型復(fù)雜測(cè)試套件中使用共享對(duì)象。此外,當(dāng)產(chǎn)品級(jí)API在缺少線程安全(可能是因?yàn)闆](méi)有需要)的情況下,被并發(fā)調(diào)用時(shí),情況就會(huì)更加惡化。
小結(jié)
在上文中,我們分析了Go語(yǔ)言里的各種數(shù)據(jù)競(jìng)爭(zhēng)模式,并對(duì)其背后的原因進(jìn)行了分類。當(dāng)然,不同的原因也可能會(huì)相互作用與影響。下表是對(duì)各種問(wèn)題的匯總。

圖7:數(shù)據(jù)競(jìng)爭(zhēng)待分類。
上面討論的主要是基于我們?cè)赨ber的Go monorepo中發(fā)現(xiàn)的各種數(shù)據(jù)競(jìng)爭(zhēng)模式,難免有些掛一漏萬(wàn)。其實(shí),代碼的交錯(cuò)覆蓋也可能產(chǎn)生數(shù)據(jù)競(jìng)爭(zhēng)模式。希望上述提到的各種經(jīng)驗(yàn)?zāi)軌驇椭嗟腉o開發(fā)人員,去關(guān)注并發(fā)代碼的編寫,考慮不同的語(yǔ)言的特性、以及避免由于自身編程習(xí)慣所引發(fā)的并發(fā)錯(cuò)誤。
到此這篇關(guān)于Go語(yǔ)言中的數(shù)據(jù)競(jìng)爭(zhēng)模式詳解的文章就介紹到這了,更多相關(guān)Go數(shù)據(jù)競(jìng)爭(zhēng)模式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解go基于viper實(shí)現(xiàn)配置文件熱更新及其源碼分析
這篇文章主要介紹了詳解go基于viper實(shí)現(xiàn)配置文件熱更新及其源碼分析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
golang將多路復(fù)異步io轉(zhuǎn)成阻塞io的方法詳解
常見(jiàn)的IO模型有阻塞、非阻塞、IO多路復(fù)用,異,下面這篇文章主要給大家介紹了關(guān)于golang將多路復(fù)異步io轉(zhuǎn)成阻塞io的方法,文中給出了詳細(xì)的示例代碼,需要的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-09-09
Go語(yǔ)言擴(kuò)展原語(yǔ)之ErrGroup的用法詳解
除標(biāo)準(zhǔn)庫(kù)中提供的同步原語(yǔ)外,Go語(yǔ)言還在子倉(cāng)庫(kù)sync中提供了4種擴(kuò)展原語(yǔ),本文主要為大家介紹的是其中的golang/sync/errgroup.Group,感興趣的小伙伴可以了解一下2023-07-07
詳解如何在golang項(xiàng)目開發(fā)中創(chuàng)建自己的Module
既然我們使用了很多開源的 module為我們的日常開發(fā)提供了很多的便捷性,那我們?cè)撊绾螌?shí)現(xiàn)自己的 module 來(lái)提供給團(tuán)隊(duì)中使用,接下小編就給大家介紹一下在golang項(xiàng)目開發(fā)如何創(chuàng)建自己的Module,需要的朋友可以參考下2023-09-09

