在傳統的編程語言中,如C++、Java、Python等,其并發邏輯多建立在操作系統線程之上。線程間的通信通常依賴于操作系統提供的基礎原語,包括共享內存、信號、管道、消息隊列及套接字等,其中共享內存是最為普遍的通信方式。但這種基于共享內存的并發模型在復雜或大規模業務場景下往往顯得復雜且易于出錯。
Go語言在設計時即以解決傳統并發問題為目標,融入了CSP(Communicating Sequential Processes,通信順序進程)模型的理念。CSP模型致力于簡化并發編程,目標是讓編寫并發程序的難度與順序程序相當。
在CSP模型中,通信和同步通過一種特定的流程實現:生產者產生數據,然后通過輸出數據到輸入/輸出原語,最終到達消費者。Go語言為實現CSP模型,特別引入了Channel機制。Goroutine可以通過Channel進行數據的讀寫操作,Channel作為連接多個Goroutine的通信橋梁,簡化了并發編程的復雜性。
雖然CSP模型在Go語言中占據主流地位,但Go同樣支持基于共享內存的并發模型。在Go的sync
包中,提供了包括互斥鎖、讀寫鎖、條件變量和原子操作等在內的多種同步機制,以滿足不同并發場景下的需求。
互斥鎖(Mutex)是一種用于在并發環境中安全訪問共享資源的機制。當一個協程獲取到鎖時,它將擁有臨界區的訪問權,而其他請求該鎖的協程將會阻塞,直到該鎖被釋放。
并發訪問共享資源的情形非常普遍,例如:
如果沒有互斥鎖的控制,將會導致商品超賣、變量數值不正確、用戶信息更新錯誤等問題。這時候就需要使用互斥鎖來控制并發訪問。
Mutex實現了Locker接口,提供了兩個方法:Lock
和Unlock
。
package mainimport ( "fmt" "sync" "time")func main() { var mu sync.Mutex var count int increment := func() { mu.Lock() defer mu.Unlock() count++ fmt.Println("Count:", count) } for i := 0; i < 5; i++ { go increment() } time.Sleep(time.Second)}
func example() { var mu sync.Mutex mu.Lock() defer mu.Unlock() // Do something... mu.Lock() // 死鎖}
func example() { var mu sync.Mutex mu.Lock() // 未調用mu.Unlock() mu.Unlock() // 正確}
func example() { var mu sync.Mutex copyMu := mu copyMu.Lock() // 錯誤}
Mutex結構體有兩個字段:state和sema。
type Mutex struct { state int32 sema uint32}
Mutex有以下四種狀態。
以下代碼展示了如何使用Mutex在并發環境中安全地訪問共享資源:
package mainimport ( "fmt" "sync" "time")func main() { var mu sync.Mutex var count int increment := func() { mu.Lock() defer mu.Unlock() count++ fmt.Println("Count:", count) } for i := 0; i < 5; i++ { go increment() } time.Sleep(time.Second)}
在上述代碼中,多個goroutine同時調用increment函數,通過Mutex來確保對共享變量count的訪問是安全的。
在并發編程中,為了保證多個協程安全地訪問共享資源,我們通常使用Mutex互斥鎖。然而,在讀多寫少的場景下,Mutex會導致性能問題,因為所有操作(包括讀操作)都必須串行進行。為了解決這一問題,可以區分讀操作和寫操作。RWMutex是一種讀寫鎖,同一時間只能被一個寫操作持有,或者被多個讀操作持有。
RWMutex提供了五個方法:Lock、Unlock、RLock、RUnlock和RLocker。
package mainimport ( "fmt" "sync" "time")func main() { var rw sync.RWMutex var count int write := func() { rw.Lock() defer rw.Unlock() count++ fmt.Println("Write:", count) } read := func() { rw.RLock() defer rw.RUnlock() fmt.Println("Read:", count) } // Start multiple readers for i := 0; i < 5; i++ { go read() } // Start a single writer go write() time.Sleep(time.Second)}
RWMutex主要通過readerCount字段來維護讀鎖的數量。寫操作時,會將readerCount減去2的30次方變成一個負數,從而阻塞新的讀鎖請求。當寫鎖被釋放時,將readerCount加上2的30次方,恢復成一個整數并喚醒等待中的讀鎖操作。
RWMutex的易錯場景和Mutex類似,包括以下幾點。
func example() { var rw sync.RWMutex rw.Lock() defer rw.Unlock() // Do something... rw.Lock() // 死鎖}
func example() { var rw sync.RWMutex rw.Lock() // 未調用rw.Unlock() rw.Unlock() // 正確}
func example() { var rw sync.RWMutex copyRw := rw copyRw.Lock() // 錯誤}
package mainimport ( "fmt" "sync" "time")func main() { var rw sync.RWMutex var count int write := func() { rw.Lock() defer rw.Unlock() count++ fmt.Println("Write:", count) } read := func() { rw.RLock() defer rw.RUnlock() fmt.Println("Read:", count) } // 啟動多個讀操作 for i := 0; i < 5; i++ { go read() } // 啟動寫操作 go write() time.Sleep(time.Second)}
在上述代碼中,多個goroutine同時調用read函數,通過RWMutex來確保對共享變量count的讀取是安全的。同時,write函數用于更新共享變量count,確保在寫操作時獨占訪問權。
死鎖指的是一組進程由于相互持有和等待資源,導致無法繼續執行的狀態。在這種情況下,所有相關的進程都會無限期阻塞,無法向前推進。具體來說,死鎖發生在一個進程持有某些資源并等待其他進程釋放其占有的資源,同時這些其他進程也在等待第一個進程釋放資源,形成相互等待的狀態。
死鎖的發生需要滿足以下四個必要條件。
為了解決死鎖問題,可以采取以下兩種策略。
以下是一個Go語言中的死鎖示例,展示了兩個goroutine由于相互等待對方持有的資源而導致的死鎖:
package mainimport ( "fmt" "sync")func main() { var mutexA, mutexB sync.Mutex go func() { mutexA.Lock() fmt.Println("Goroutine 1: Locked mutexA") // Simulate some work mutexB.Lock() fmt.Println("Goroutine 1: Locked mutexB") mutexB.Unlock() mutexA.Unlock() }() go func() { mutexB.Lock() fmt.Println("Goroutine 2: Locked mutexB") // Simulate some work mutexA.Lock() fmt.Println("Goroutine 2: Locked mutexA") mutexA.Unlock() mutexB.Unlock() }() // Wait for goroutines to finish (they won't due to deadlock) select {}}
在上述代碼中,兩個goroutine分別持有mutexA
和mutexB
,并且嘗試獲取對方的鎖,導致死鎖發生。每個goroutine無限期等待對方釋放資源,形成相互等待的循環。
通過了解死鎖的概念、必要條件及解決策略,我們可以更好地設計并發程序,避免陷入死鎖狀態。
WaitGroup 是 Go 語言的 sync 包下提供的一種并發原語,用來解決并發編排的問題。它主要用于等待一組 goroutine 完成。假設一個大任務需要等待三個小任務完成才能繼續執行,如果采用輪詢的方法,可能會導致兩個問題:一是小任務已經完成但大任務需要很久才能被輪詢到,二是輪詢會造成 CPU 資源的浪費。因此,WaitGroup 通過阻塞等待并喚醒大任務的 goroutine 來解決這個問題。
WaitGroup 提供了三個方法:Add、Done 和 Wait。
WaitGroup 維護了兩個計數器,一個是 v 計數器,另一個是 w 計數器。
使用 WaitGroup 需要注意以下易錯場景:
以下是一個使用 WaitGroup 的示例代碼:
package mainimport ( "fmt" "sync" "time")func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // Done() 方法用于減少計數器 fmt.Printf("Worker %d starting/n", id) time.Sleep(time.Second) fmt.Printf("Worker %d done/n", id)}func main() { var wg sync.WaitGroup for i := 1; i <= 3; i++ { wg.Add(1) // Add() 方法增加計數器 go worker(i, &wg) } wg.Wait() // Wait() 方法阻塞等待所有計數器為 0 fmt.Println("All workers done")}
在上述代碼中,main 函數創建了一個 WaitGroup 并啟動了三個 goroutine,每個 goroutine 執行 worker 函數。在 worker 函數中,調用 wg.Done() 方法表示當前工作已經完成。main 函數中的 wg.Wait() 方法阻塞等待,直到所有的 goroutine 都完成工作并調用了 Done 方法。
WaitGroup 是 Go 語言中非常有用的并發原語,用于等待一組 goroutine 完成。通過合理使用 Add、Done 和 Wait 方法,可以避免輪詢等待帶來的性能問題,并提高并發編排的效率。在使用 WaitGroup 時,需要注意計數器的增減操作,避免引發 panic 或長時間阻塞。
Go 語言提倡通過通信來實現共享內存,而不是通過共享內存來通信。Go 的 CSP(Communicating Sequential Processes)并發模型正是通過 Goroutine 和 Channel 來實現的。Channel 是 Go 語言中用于 goroutine 之間通信的主要工具。
Channel 有以下幾類應用場景。
Channel 有三種類型:
Channel 通過 make 函數進行初始化,未初始化的 Channel 的零值是 nil,對 nil 的 Channel 進行接收或發送操作會導致阻塞。
Channel 可以分為有緩沖和無緩沖兩種。無緩沖的 Channel 是同步的,有緩沖的 Channel 是異步的。發送操作只有在 Channel 滿時才會阻塞,接收操作只有在 Channel 為空時才會阻塞。
發送操作是 chan<-,接收操作是 <-chan。接收數據時可以返回兩個值,第一個是元素,第二個是一個布爾值,若為 false 則說明 Channel 已經被關閉并且 Channel 中沒有緩存的數據。
Go 的內建函數 close、cap、len 都可以操作 Channel 類型,發送和接收都可以作為 select 語句的 case,Channel 也可以應用于 for range 語句。
在發送數據給 Channel 時,發送語句會轉化為 chansend 函數:
在接收數據時,接收語句會轉化為 chanrecv 函數:
以下是一個使用 Channel 的示例代碼:
package mainimport ( "fmt" "time")// 生產者:生成數據并發送到 channelfunc producer(ch chan<- int, count int) { for i := 0; i < count; i++ { ch <- i fmt.Println("Produced:", i) time.Sleep(time.Millisecond * 500) } close(ch) // 關閉 channel,表示生產結束}// 消費者:從 channel 接收數據并處理func consumer(ch <-chan int) { for data := range ch { fmt.Println("Consumed:", data) time.Sleep(time.Millisecond * 1000) }}func main() { ch := make(chan int, 5) // 創建一個帶緩沖的 channel go producer(ch, 10) // 啟動生產者 consumer(ch) // 啟動消費者}
在上述代碼中,main 函數創建了一個帶緩沖的 Channel,并啟動了一個生產者 goroutine 和一個消費者 goroutine。生產者不斷生成數據并發送到 Channel 中,消費者從 Channel 中接收數據并進行處理。生產者完成后關閉 Channel,消費者則在接收到所有數據后結束。
本文鏈接:http://www.tebozhan.com/showinfo-26-112752-0.htmlGo并發編程詳解鎖、WaitGroup、Channel
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com