欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Go語言中的數(shù)據(jù)競爭模式詳解

 更新時間:2022年07月20日 15:07:11   作者:WOT技術大會???????  
這篇文章主要介紹了Go語言中的數(shù)據(jù)競爭模式詳解,主要基于在Uber的Go monorepo中發(fā)現(xiàn)的各種數(shù)據(jù)競爭模式,分析了其背后的原因與分類,需要的朋友可以參考一下

前言

本文主要基于在Uber的Go monorepo中發(fā)現(xiàn)的各種數(shù)據(jù)競爭模式,分析了其背后的原因與分類,希望能夠幫助更多的Go開發(fā)人員,去關注并發(fā)代碼的編寫,考慮不同的語言的特性、以及避免由于自身編程習慣所引發(fā)的并發(fā)錯誤。

近年來,Uber已經(jīng)開始采用Golang(簡稱Go)作為開發(fā)微服務的主要編程語言。目前,其Go monorepo(譯者注:包含多個不同項目的單個倉庫)包含了大約5,000萬行代碼,以及大約2,100個獨特的Go服務。而且,它們都還在持續(xù)增長中。

為了實現(xiàn)并發(fā),我們通常會使用go關鍵字,為函數(shù)調用添加前綴,以實現(xiàn)異步式的運行調用。在Go中,此類異步函數(shù)調用被稱為goroutine。開發(fā)人員可以通過創(chuàng)建goroutine(例如,對其他服務的IO或RPC調用),來隱藏延遲。不同的goroutine可以通過消息傳遞,以及共享內存的方式,來傳遞數(shù)據(jù)。其中,共享內存恰好是Go中最常用的數(shù)據(jù)通信方式之一。

由于goroutineGo很容易被程序員創(chuàng)建和使用,因此它被認為屬于“輕量級” 。同時,由Go編寫的程序通常會比由其他語言編寫的程序具有更強的并發(fā)性。例如,通過掃描數(shù)十萬個運行在數(shù)據(jù)中心的微服務實例,我們發(fā)現(xiàn)Go微服務的并發(fā)性可達Java微服務的8倍。

當然,更高的并發(fā)性也意味著更多潛在的并發(fā)錯誤。我們常用數(shù)據(jù)競爭(data race)來描述當兩個或多個goroutine訪問相同的數(shù)據(jù),而且至少有一個處于寫入狀態(tài)時,由于它們之間并沒有排序,因此就會發(fā)生并發(fā)錯誤??偟膩碚f,根據(jù)Go自身的相互作用等特點,數(shù)據(jù)競爭之類的隱蔽錯誤非常容易出現(xiàn),因此我們應該盡量避免。

最近,我們使用動態(tài)數(shù)據(jù)競爭檢測技術開發(fā)了一個系統(tǒng),專門用來檢測Uber的數(shù)據(jù)競爭。它在上線的六個月時間內,在我們的Go代碼庫中,檢測到了大約2,000個數(shù)據(jù)競爭。其中已被開發(fā)人員著手修復了的數(shù)據(jù)競爭約有1,100個。下面,我將向您展示我們已發(fā)現(xiàn)的各種常見數(shù)據(jù)競爭模式。

Go在goroutine中通過引用來透明地捕獲自由變量 

Go中的嵌套函數(shù)(又名closure)通過引用的方式,透明地捕獲所有自由的變量。程序員通常無需明確指定在closure語法中,需要捕獲哪些自由變量。

這種方式是有別于Java和C++的。Java的lambda僅會根據(jù)數(shù)值去捕獲,而且他們會有意識地避免并發(fā)缺陷。而C++則要求開發(fā)人員明確地指明是使用數(shù)值、還是引用的捕獲方式。

當closure較大時,開發(fā)人員并不知道closure內使用的變量是否自由,可否通過引用來捕獲。而由于引用的捕獲、以及goroutine都是并發(fā)的,因此Go程序最終可能會因為沒能顯式地執(zhí)行同步,而對自由變量進行無序的訪問。我們可以通過如下三個示例來證明這一點:

示例1:由循環(huán)索引的變量捕獲,而導致數(shù)據(jù)競爭

圖1A中的代碼顯示了迭代Go的切片作業(yè),并通過ProcessJob函數(shù)來處理每個元素的作業(yè)。

Go語言中的數(shù)據(jù)競爭模式_數(shù)據(jù)_02

圖1A:由循環(huán)索引的變量捕獲,而導致數(shù)據(jù)競爭。

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

示例2:由err變量的捕獲,所導致的數(shù)據(jù)競爭

Go語言中的數(shù)據(jù)競爭模式_java_03

圖1B:由err變量的捕獲,所導致的數(shù)據(jù)競爭。

Go一直提倡函數(shù)有多個返回值。圖1B展示了一種常見的通過返回實際值和錯誤對象,來指示是否存在錯誤的用法。可見,當且僅當錯誤值為nil(空)時,實際的返回值才會被認為是有意義的。因此,我們的通常做法是:將返回的錯誤對象,分配給名為err的變量,然后檢查其是否為空(nilness)。不過,由于我們可以在函數(shù)體內調用多個返回錯誤的函數(shù),因此程序每次都會對err變量進行多次賦值,然后進行是否為空的檢查。當開發(fā)人員將這個習慣用法與goroutine混合使用時,錯誤變量就會在closure中被引用捕獲。結果,程序對于goroutine中err的讀寫訪問,與隨后對封閉函數(shù)(或goroutine的多個實例)中相同的err變量的讀寫操作,就會同時運行。這便導致了數(shù)據(jù)競爭。

示例3:由已命名的返回變量捕獲,所導致的數(shù)據(jù)競爭

Go語言中的數(shù)據(jù)競爭模式_java_04

圖1C:由已命名的返回變量捕獲,所導致的數(shù)據(jù)競爭。

Go引入了一種被稱為已命名返回值的語法塊。已命名的返回變量被視為在函數(shù)頂部定義的變量,其作用域超出了函數(shù)體。而沒有參數(shù)的return語句,被稱為“裸”命名返回值。由于closure的存在,如果將正常(非裸)的返回與已命名的返回相混合、或在具有命名返回的函數(shù)中使用延遲返回,那么就可能會引發(fā)數(shù)據(jù)競爭。

在上圖1C中的NamedReturnCallee函數(shù)返回了一個整數(shù),而且返回變量被命名為result。根據(jù)該語法,函數(shù)體的其余部分可以對結果進行直接讀寫,而無需額外聲明。如果函數(shù)在第4行返回的是一個裸返回,而由于在第2行被賦值為result=10,那么第13行的調用者將看到其返回值為10。編譯器則會安排將結果復制到retVal。同時,已命名的返回函數(shù)也可以使用如第9行所示的標準返回語法。該語法會讓編譯器復制return語句中的返回值20,以分配給已命名的返回變量結果。第6行創(chuàng)建了一個goroutine,它會捕獲已命名的返回變量的結果。在設置該goroutine時,即使是并發(fā)專家也可能認為讀取第7行的結果中是安全的,畢竟不存在對同一變量的寫入,而且第9行的語句返回的20是一個常量,它似乎并沒有觸及到已命名的返回變量結果。不過,如前所述,代碼在生成的過程中,會將return 20的語句轉換為寫入結果。此時,一旦我們突然對共享的結果變量進行并發(fā)讀寫,就會產(chǎn)生數(shù)據(jù)競爭的情況。

切片會產(chǎn)生難以診斷的數(shù)據(jù)競爭 

切片(Slices)實際上是一些動態(tài)數(shù)組和引用類型。在其內部,切片包含了一個指向底層數(shù)組的指針、它的當前長度、以及底層數(shù)組可以擴展的最大容量。為了便于討論,我們將這些變量統(tǒng)稱為切片的元字段(meta field)。切片上的一種常見操作便是通過追加操作(append operation)來使其增長。當達到其容量限制時,代碼會進行新的分配(例如,對當前的容量翻倍),并更新其對應的元字段。而當一個切片被goroutine并發(fā)訪問時,Go會通過互斥鎖(mutex),來保護對它的訪問。

Go語言中的數(shù)據(jù)競爭模式_數(shù)據(jù)_05

圖2:即使使用鎖,切片仍會出現(xiàn)數(shù)據(jù)競爭。

在圖2中,開發(fā)人員往往以為已經(jīng)對第6行的切片進行了鎖定保護,便可防止數(shù)據(jù)競爭的出現(xiàn)。而實際上,當?shù)?4行將切片作為參數(shù)傳遞給沒有鎖保護的goroutine時,就會產(chǎn)生數(shù)據(jù)競爭。具體而言,goroutine的調用導致了切片中的元字段從調用處(第14行)被復制到被調用者(第11行)處??紤]到切片屬于引用類型,我們認為在將其傳遞(復制)到被調用者時,會導致數(shù)據(jù)競爭的發(fā)生。不過,由于切片與指針類型不同,畢竟元字段是按照數(shù)值復制的,因此該數(shù)據(jù)競爭的發(fā)生概率非常低。

并發(fā)訪問Go內置的、不安全的線程映射會導致頻繁的數(shù)據(jù)競爭 

哈希表(或稱映射)是Go中的內置語言功能。不過,它對于線程是不安全的。如果多個goroutine同時訪問同一張哈希表,而且其中至少有一個試圖去修改哈希表(插入或刪除某項)的話,就會產(chǎn)生數(shù)據(jù)競爭。開發(fā)人員往往認為他們可以同時訪問哈希表中的不同項。而實際上,與數(shù)組或切片不同,映射(哈希表)是一種稀疏的數(shù)據(jù)結構,訪問某一個元素就可能會導致訪問另一個元素,如果在同一過程中發(fā)生了另一種插入或刪除,那么它將會因為修改了稀疏的數(shù)據(jù)結構,而導致了數(shù)據(jù)競爭。

我們甚至觀察到了更為復雜的、由并發(fā)映射訪問產(chǎn)生的數(shù)據(jù)競爭。其原因是同一個哈希表被傳遞到了深度調用路徑,而開發(fā)人員忘記了這些調用路徑是通過異步goroutine去改變哈希表的事實。圖3便顯示了此類數(shù)據(jù)競爭的示例。

Go語言中的數(shù)據(jù)競爭模式_java_06

圖3:由于并發(fā)映射訪問導致的數(shù)據(jù)競爭。

雖然導致數(shù)據(jù)競爭的哈希表并非Go獨有,但是以下原因會讓Go更容易發(fā)生數(shù)據(jù)競爭:

  • 由于映射是一種內置的語言結構,因此Go開發(fā)人員會比其他語言的開發(fā)者更頻繁地使用映射。例如,在我們的Java存儲庫中,每MLoC(Millions of Lines Of Code,數(shù)百萬行代碼)里有4,389個映射結構;而在Go中,每MLoC里就有5,950個映射,足足高出了1.34倍。
  • 不同于Java的get和put API,哈希表的訪問語法類似數(shù)組訪問語法,雖然易于使用,但是也會意外地與隨機訪問數(shù)據(jù)結構相混淆。在Go中,我們可以使用table[key]的語法,輕松查詢那些不存在(non-existing)的映射元素。該語法能夠簡單地返回默認值,而不會產(chǎn)生任何錯誤。這種容錯性對于開發(fā)者在使用Go的映射時是非常友好的。

Go開發(fā)人員常在pass-by-value時犯錯并導致non-trivial的數(shù)據(jù)競爭

Go建議使用pass-by-value的語義,以簡化逃逸分析,并為變量提供更好的棧上分配的機會,進而減少垃圾收集器的壓力。

與所有對象皆為引用類型的Java不同,在Go中,對象可以是數(shù)值類型(如:結構),也可以是引用類型(如:接口)。由于沒有了語法差異,這會導致諸如:sync.Mutex和sync.RWMutex等數(shù)值類型,在同步構造中被錯誤地使用。如果一個函數(shù)創(chuàng)建了一個互斥體結構,并通過數(shù)值傳遞(pass-by-value)給多個goroutine調用,那么這些goroutine在并發(fā)執(zhí)行時,不同的互斥對象是不會在操作過程中共享內部狀態(tài)的。這也就破壞了對于受保護的共享內存區(qū)域的互斥訪問特性。請參見如下圖4所示的代碼。

Go語言中的數(shù)據(jù)競爭模式_數(shù)據(jù)_07

圖4A:

由by-reference或by-pointer的方法調用所引起的數(shù)據(jù)競爭

Go語言中的數(shù)據(jù)競爭模式_數(shù)據(jù)_08

圖4B:sync.Mutex的Lock/Unlock簽名。

由于Go語法在指針和數(shù)值上調用方法是相同的,因此開發(fā)人員往往會忽視m.Lock()正在處理互斥鎖的副本并非指針這一問題。調用者仍然可以在互斥的數(shù)值上調用這些API。而且編譯器也會透明地安排傳遞數(shù)值的地址。相反,如果沒有此類透明度,該錯誤就能夠會被檢測到,并認定為編譯器類型不匹配的錯誤。

據(jù)此,當開發(fā)人員意外地實現(xiàn)了一個方法,其中的接收者是指向結構的指針,而不是結構的數(shù)值或副本時,那么就會發(fā)生與此相反的情況。也就是說,調用該方法的多個goroutine,最終會意外地共享結構相同的內部狀態(tài)。而且,調用者也不會意識到數(shù)值類型在接收者處被透明地轉換為了指針類型。顯然,這都是開發(fā)人員所不愿發(fā)生的。

消息傳遞(通道)和共享內存的混合使用使代碼變得復雜且易受數(shù)據(jù)競爭的影響

Go語言中的數(shù)據(jù)競爭模式_開發(fā)人員_09

圖5:將消息傳遞與共享內存混合時的數(shù)據(jù)競爭。

圖5展示了開發(fā)人員使用一個專門為信號和等待準備的通道,通過Future來實現(xiàn)的示例。我們可以通過調用Start()方法來啟動Future,并通過調用Future的Wait()方法,來阻止Future的完成。Start()方法會創(chuàng)建一個goroutine,以執(zhí)行一個注冊到Future的函數(shù),并記錄其返回值(如:response和err)。如第6行所示,goroutine通過在通道ch上發(fā)送一條消息,以向Wait()方法發(fā)出Future完成的信號。對稱地,如第11行所示,Wait()方法塊會從通道中獲取相應的消息。

在Go中,上下文攜帶了跨越API邊界和進程之間的截止日期、取消信號和其他請求范圍的數(shù)值。這是在微服務中為任務設置時間線的常見模式。由此,Wait()阻止了被取消(第13行)的上下文、或已完成的Future(第11行)。此外,Wait()被包裝在一個select語句(第10行)中,并處于阻止狀態(tài),直到至少有一個選擇arm準備就緒。

如果上下文超時,則相應的案例將Future的err字段,在第14行上記錄為ErrCancelled。此時,對于err的寫入與第5行對Future的相同變量的寫入操作,便形成了競爭。

Add和Done方法的錯誤放置會導致數(shù)據(jù)競爭

sync.WaitGroup結構是Go的組同步結構。與C++的barrier的barrier、以及l(fā)atch的構造不同,WaitGroup中參與者的數(shù)量不是在構造時被確定的,而是動態(tài)更新的。在WaitGroup對象上,Go允許進行Add(int)、Done()和Wait()三種操作。其中,Add()會增加參與者的計數(shù),而Wait()會處于阻止狀態(tài),直到Done()被調用為count的次數(shù)(通常每個參與者一次)。由于在Go中,組同步的使用程度比Java高出1.9倍,因此WaitGroup在Go中常被廣泛地使用。

在下圖6中,開發(fā)人員打算創(chuàng)建與切片itemId里的元素數(shù)量相同的goroutine,且并發(fā)處理它們。每個goroutine在不同索引的結果切片、以及在第12行對父功能塊中,記錄其成功或失敗的狀態(tài),直到所有的goroutine已完成。接著,它會依次訪問結果中的所有元素,以計算出被成功處理的數(shù)量。

Go語言中的數(shù)據(jù)競爭模式_java_10

圖6A:

由于WaitGroup.Add()的錯誤放置,導致了數(shù)據(jù)競爭

為了使該代碼能夠正常工作,我們需要在第12行調用Wait()時,保證wg.Add(1)在調用wg.Wait()之前所執(zhí)行的次數(shù),也就是注冊參與者的數(shù)量,必須等于itemIds的長度。這就意味著wg.Add(1)應該在每個goroutine之前被放置在第5行調用。但是,如果開發(fā)人員在第7行錯誤地將wg.Add(1)放置在了goroutine的主體中,它就無法保證在外部函數(shù)WaitGrpExample調用Wait()時,完整地執(zhí)行。據(jù)此,在調用Wait()時,被注冊到WaitGroup的itemId的長度就可能會變短。正是出于該原因,Wait()會被提前解除阻止。據(jù)此,WaitGrpExample函數(shù)則可以從切片結果中開始讀?。矗旱?3行),而一些goroutine則開始并發(fā)寫入同一個切片。

此外,我們還發(fā)現(xiàn)過早地在Waitgroup上調用wg.Done(),也會導致數(shù)據(jù)競爭。下圖6B展示了wg.Done()與Go的defer語句交互的結果。當遇到多個defer語句時,代碼會按照“后進先出”的順序去執(zhí)行。其中,第9行的wg.Wait()會在doCleanup()運行之前完成。即,父goroutine會在第10行去訪問locationErr,而子goroutine可能仍然在延遲的doCleanup()函數(shù)內寫入locationErr(為簡潔起見,在此并未顯示)。

Go語言中的數(shù)據(jù)競爭模式_java_11

圖6B:由于WaitGroup.Done()的錯誤放置

延遲語句排序,并導致了數(shù)據(jù)競爭。

并發(fā)運行測試會導致產(chǎn)品或測試代碼中的數(shù)據(jù)競爭 

測試是Go的內置功能。在那些后綴為_test.go的文件里,任何前綴為Test的函數(shù),都可以測試由Go構建的系統(tǒng)。如果測試代碼調用了API--testing.T.Parallel(),那么它將與其他同類測試并發(fā)運行。我們發(fā)現(xiàn)此類并發(fā)測試有時會在測試代碼中、有時也會在產(chǎn)品代碼中產(chǎn)生大量的數(shù)據(jù)競爭。

此外,在單個以Test為前綴的函數(shù)中,Go開發(fā)人員經(jīng)常會編寫許多子測試,并通過由Go提供的套件包去執(zhí)行它們。Go推薦開發(fā)人員通過表驅動的測試套件習語(table-driven test suite idiom)去編寫和運行測試套件。據(jù)此,我們的開發(fā)人員在同一個測試中就編寫了數(shù)十、甚至數(shù)百個可供系統(tǒng)并發(fā)運行的子測試。開發(fā)人員以為代碼會執(zhí)行串行測試,而忘記了在大型復雜測試套件中使用共享對象。此外,當產(chǎn)品級API在缺少線程安全(可能是因為沒有需要)的情況下,被并發(fā)調用時,情況就會更加惡化。

小結 

在上文中,我們分析了Go語言里的各種數(shù)據(jù)競爭模式,并對其背后的原因進行了分類。當然,不同的原因也可能會相互作用與影響。下表是對各種問題的匯總。

Go語言中的數(shù)據(jù)競爭模式_開發(fā)人員_12

圖7:數(shù)據(jù)競爭待分類。

上面討論的主要是基于我們在Uber的Go monorepo中發(fā)現(xiàn)的各種數(shù)據(jù)競爭模式,難免有些掛一漏萬。其實,代碼的交錯覆蓋也可能產(chǎn)生數(shù)據(jù)競爭模式。希望上述提到的各種經(jīng)驗能夠幫助更多的Go開發(fā)人員,去關注并發(fā)代碼的編寫,考慮不同的語言的特性、以及避免由于自身編程習慣所引發(fā)的并發(fā)錯誤。

到此這篇關于Go語言中的數(shù)據(jù)競爭模式詳解的文章就介紹到這了,更多相關Go數(shù)據(jù)競爭模式內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Go設計模式之中介者模式講解和代碼示例

    Go設計模式之中介者模式講解和代碼示例

    中介者是一種行為設計模式,讓程序組件通過特殊的中介者對象進行間接溝通,達到減少組件之間依賴關系的目的,因此本文就給大家詳細介紹一下Go中介者模式,需要的朋友可以參考下
    2023-06-06
  • k8s容器互聯(lián)flannel?vxlan通信原理

    k8s容器互聯(lián)flannel?vxlan通信原理

    這篇文章主要為大家介紹了k8s容器互聯(lián)flannel?vxlan通信原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-04-04
  • vscode上搭建go開發(fā)環(huán)境詳細完整過程

    vscode上搭建go開發(fā)環(huán)境詳細完整過程

    這篇文章主要給大家介紹了關于vscode上搭建go開發(fā)環(huán)境的詳細完整過程,Go語言或將成為新的主力開發(fā)語言,Go是google開發(fā)的一種靜態(tài)強類型、編譯型、并發(fā)型,并具有垃圾回收功能的編程語言,所以我們有必要學習并掌握它,需要的朋友可以參考下
    2023-10-10
  • Golang Recover處理錯誤原理解析

    Golang Recover處理錯誤原理解析

    Golang 中的?recover?是一個鮮為人知但非常有趣和強大的功能,讓我們看看它是如何工作的,以及在 Outreach.io 中如何利用它來處理 Kubernetes 中的錯誤
    2023-12-12
  • 詳解Golang?ProtoBuf的基本語法總結

    詳解Golang?ProtoBuf的基本語法總結

    最近項目是采用微服務架構開發(fā)的,各服務之間通過gPRC調用,基于ProtoBuf序列化協(xié)議進行數(shù)據(jù)通信,因此接觸學習了Protobuf,本文會對Protobuf的語法做下總結,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助
    2022-10-10
  • Golang token的生成和解析詳解

    Golang token的生成和解析詳解

    這篇文章主要給大家介紹了Golang token的生成和解析,文中通過代碼示例給大家介紹的非常詳細,對大家的學習或工作有一定的幫助,需要的朋友可以參考下
    2024-02-02
  • 利用go語言實現(xiàn)查找二叉樹中的最大寬度

    利用go語言實現(xiàn)查找二叉樹中的最大寬度

    這篇文章主要介紹了利用go語言實現(xiàn)查找二叉樹中的最大寬度,文章圍繞主題展開詳細介紹,具有一定的參考價值,需要的小伙伴可以參考一下
    2022-05-05
  • GO語言gin框架實現(xiàn)管理員認證登陸接口

    GO語言gin框架實現(xiàn)管理員認證登陸接口

    這篇文章主要介紹了GO語言gin框架實現(xiàn)管理員認證登陸接口,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-10-10
  • golang grpc配置使用實戰(zhàn)

    golang grpc配置使用實戰(zhàn)

    本文主要介紹了golang grpc配置使用實戰(zhàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-05-05
  • 詳解go-admin在線開發(fā)平臺學習(安裝、配置、啟動)

    詳解go-admin在線開發(fā)平臺學習(安裝、配置、啟動)

    這篇文章主要介紹了go-admin在線開發(fā)平臺學習(安裝、配置、啟動),本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-02-02

最新評論