Go 併發程式設計

2022-07-16 17:30:19 字數 4601 閱讀 4591

go語言宣揚用通訊的方式共享資料。

go語言以獨特的併發程式設計模型傲視群雄,與併發程式設計關係最緊密的**包就是sync包,意思是同步。同步的用途有兩個,乙個是避免多個執行緒在同一時刻操作同乙個資料塊,另乙個是協調多個執行緒,以避免它們在同一時刻執行同一塊**。由於這一的資料庫和**塊的背後都隱含著一種或多種資源,所以可以把它們看成是共享資源,同步就是控制多個執行緒對共享資源的訪問。

乙個執行緒在想要訪問某乙個共享資源時,需要先申請對該資源的訪問許可權,並且只有在申請成功之後,訪問才能真正開始,而當執行緒對共享資源的訪問結束時,它還必須歸還對該資源的訪問許可權,若要再次訪問仍需申請。多個併發執行的執行緒對乙個共享資源的訪問是完全序列的。

在go語言中,最常用的同步工具當屬互斥量(mutex)。sync包中的mutex就是與其對應的型別,該型別的值可以被稱為互斥量或互斥鎖。乙個互斥鎖可以被用來保護乙個臨界區或者一組臨界區,可以通過它來保證,在同一時刻只有乙個goroutin處於該臨界區之內。每當有goroutine想進入臨界區時,都需要先對它進行鎖定,並且每個goroutine離開臨界區時,都要及時對它進行解鎖。鎖定操作可以通過呼叫互斥鎖的lock方法實現,解鎖操作可以呼叫互斥鎖的unlock方法

mu.lock()

_, err := writer.write(byte

(data))

if err !=nil

mu.unlock()

1.不要重複鎖定互斥鎖

對乙個已經被鎖定的互斥鎖進行鎖定,會立即阻塞當前的goroutine

當go語言執行時系統發現所有的使用者級goroutine都處於等待狀態(死鎖),就會自行丟擲乙個帶有如下資訊的panic:

fatal error: all goroutines are asleep - deadlock!

這種由go語言執行時系統自行丟擲的panic屬於致命錯誤,都是無法被恢復的,呼叫recover函式對它們起不到任何作用,即一旦產生死鎖,程式必然奔潰+

避免這種情況的發生,最簡單有效的方式就是讓每乙個互斥鎖都只保護乙個臨界區

2.不要忘記解鎖互斥鎖,必要時使用defer語句

忘記解鎖會使其他goroutine無法進入到該互斥鎖保護的臨界區,這輕則會導致一些程式功能的失效,重則會造成死鎖和程式奔潰。

3.不要對尚未鎖定或者已解鎖的互斥鎖解鎖

解鎖為鎖定的鎖會立即引發panic,應該總是抱著,對每乙個鎖定操作,都要有且只有乙個對應的解鎖操作。

4.不要在多個函式之間直接傳遞互斥鎖

go語言中的互斥鎖是開箱即用的,一旦宣告了乙個sync。mutex型別的變數,就可以直接使用它。但該型別是乙個結構體型別,屬於值型別的一種,把它傳給乙個函式、將它從函式中返回、把它賦值給其他變數、讓它進入某個通道都會導致它的副本的產生。並且原值和它的副本,以及多個副本之間都是完全獨立的,它們都是不同的互斥鎖。如果把乙個互斥鎖作為引數值傳給了乙個函式,那麼在這個函式中對傳入的鎖的所有操作,都不會對存在於該函式之外的那個原鎖產生任何影響。

讀寫鎖是讀/寫互斥鎖的簡稱。在go語言中,讀寫鎖由sync.rwmutex型別的值代表,也是開箱即用的。讀寫鎖把對共享資源的讀操作和寫操作區別對待了,它可以對這兩種操作施加不同程度的保護。

乙個讀寫鎖實際上包含了兩個鎖,即:讀鎖和寫鎖。sync.rwmutex型別中的lock方法和unlock方法分別用於對寫鎖進行鎖定和解鎖,而它的rlock方法和runlock方法則分別用於對讀鎖進行鎖定和解鎖

另外,對於同乙個讀寫鎖來說有如下規則:

條件變數並不是被用來保護臨界區和共享資源的,它是用於協調想要訪問共享資源的那些執行緒的。當共享資源的狀態發生變化時,它可以被用來通知被互斥鎖阻塞的執行緒。

條件變數提供三個方法:等待通知(wait)、單發通知(signal)和廣播通知(broadcast)。在等待通知的時候需要在條件變數基於的那個互斥鎖的保護下進行。在進行單發或者廣播通知時,需要在對應互斥鎖解鎖之後做這兩種操作。

舉個栗子,兩個人在執行秘密任務,需要在不直接聯絡和見面的前提下進行,乙個人需要向信箱裡放置情報,另乙個人需要從信箱裡獲取情報,這個信箱就如同乙個共享資源。

var mailbox uint8   //

信箱,值為0表示情報,值為1表示有情報

varlock sync.rwmutex //

讀寫鎖//

sync.cond型別不是開箱即用,需要利用sync.newcond來建立。

sendcond := sync.newcond(&lock) //

*sync.cond型別

recvcond := sync.newcond(lock.rlocker()) //

*sync.cond型別

條件變數是基於互斥鎖的,因此這裡的sync.locker型別的引數值不可或缺。

sync.locker是乙個介面,在它宣告中只包含兩個方法的定義,lock()和unlock。sync.mutex和sync.rwmutex型別都擁有lock方法和unlock方法,只不過它們都是指標方法。因此這兩個型別的指標型別才算sync.locker介面的實現型別。

這裡在為sendcond做初始化時,把基於lock變數的指標值傳給了sync.newcond函式。因為lock變數的lock方法和unlock方法分別用於對寫鎖的鎖定和解鎖,它們與sendcond變數的含義是對應的。sendcond變數是專門為放置情報而準備的條件變數,向信箱中放置情報。

recvcond變數代表的是專門為獲取情報而準備的條件變數。與sendcond不同,lock變數中用於對讀鎖進行鎖定和解鎖的方法是rlock和runlock,它們與sync.locker介面中定義的方法並不匹配。需要呼叫sync.rwmutex型別的rlocker方法實現這一需求。lock.rlocker()得來的值所擁有的lock方法和unlock方法,在其內部會分別呼叫lock變數的rlock和runlock方法,即前兩個方法僅僅是後兩個方法的**。

定義好了變數,那放置情報並通知另外乙個人應該怎麼做呢

lock.lock()  //

持有信箱上的鎖,寫操作

for mailbox == 1

mailbox = 1

//放入情報

lock.unlock() //

寫完recvcond.signal()

獲取情報

lock.rlock()  //

讀操作for mailbox == 0

mailbox = 0

//取走情報

lock.runlock() //

讀完sendcond.signal()

1、把呼叫它的goroutine(當前的goroutine)加入到當前條件變數的通知佇列中

2、解鎖當前條件變數基於的那個互斥鎖

3、讓當前的goroutine處於等待狀態,等到通知到來時再決定是否喚醒它,這時這個goroutine就會阻塞在呼叫這個wait方法的那行**上

4、如果通知到來並且決定喚醒這個goroutine,那麼就在喚醒它之後重新鎖定當前條件變數基於的互斥鎖。自此以後,當前的goroutine就會繼續執行後面的**

為什麼先要鎖定條件變數基於的互斥鎖,才能呼叫它的wait方法?

那因為條件變數的wait方法在阻塞當前的goroutine之前會解鎖它基於的互斥鎖,所以在呼叫該wait方法之前,必須先鎖定那個互斥鎖,否則在呼叫這個wait方法時,會引發乙個不可恢復的panic

為什麼要用for語句包裹呼叫其wait方法的表示式,用if語句不行嗎?

顯然,if語句只會對共享資源的狀態檢查一次,for語句可以做多次檢查,直到這個狀態改變為止。

那為什麼要做多次檢查呢?

主要是為了保險起見。如果乙個goroutine因收到通知而被喚醒,但卻發現共享資源的狀態依然不符合它的要求,那麼就應該再次呼叫條件變數的wait方法,並繼續等待下次通知的到來。

那什麼時候會出現上述的情況呢?

1)有多個goroutine在等待共享資源的同一種狀態。雖然等待的goroutine很多,但每次成功的goroutine卻可能只有乙個。成功的goroutine最終解鎖互斥鎖之後,其他的goroutine會先後進入臨界區,但它們會發現共享資源狀態依然不是它們想要的。

2)共享資源狀態可能有的狀態不是兩個,如mailbox變數可能值不只有0和1,還有2,3,4。但每次改變後的結果只可能有乙個,所以單一的結果一定不可能滿足所有goroutine的條件,那些未被滿足的goroutine需要繼續等待。

3)在一些多cpu核心的計算機系統中,即使沒有收到條件變數的通知,呼叫其wait方法的goroutine也是有可能被喚醒的。這是硬體層面決定的。

條件變數的signal方法和broadcast方法都是用來傳送通知的,不同的是,前者的通知只會喚醒乙個因此而等待的goroutine,而後者的通知卻會喚醒所有為此等待的goroutine。

條件變數的wait方法總會把當前的goroutine新增到佇列的隊尾,而它的signal方法總會從通知佇列的隊首開始查詢可被喚醒的goroutine,所以,因signal方法的通知而被喚醒的goroutine一般都是最早等待的那個。

條件變數的signal方法和broadcast方法不需要在互斥鎖保護下執行。

條件變數的通知有即時性。即如果發生通知的時候沒有goroutine為此等待,那麼該通知就會被遺棄

go併發程式設計

x ch 從ch中接收值並賦值給變數x ch 從ch中接收值,忽略結果關閉 我們通過呼叫內建的close函式來關閉通道。close ch 關於關閉通道需要注意的事情是,只有在通知接收方goroutine所有的資料都傳送完畢的時候才需要關閉通道。通道是可以被垃圾 機制 的,它和關閉檔案是不一樣的,在結...

Go併發程式設計實踐

併發程式設計一直是golang區別與其他語言的很大優勢,也是實際工作場景中經常遇到的。近日筆者在組內分享了我們常見的併發場景,及 示例,以期望大家能在遇到相同場景下,能快速的想到解決方案,或者是拿這些方案與自己實現的比較,取長補短。現整理出來與大家共享。回到頂部 很多時候,我們只想併發的做一件事情,...

go語言併發程式設計

協程 coroutine 本質上是一種使用者態執行緒,不需要作業系統來進行搶占式排程,且在真正的實現中寄存於執行緒中,系統開銷極小。package main import fmt func count ch chan int,i int func main for ch range chs chan...