iT邦幫忙

2025 iThome 鐵人賽

DAY 4
1
Cloud Native

Go 語言搶票煉金術:解鎖千萬級併發下的原子交易奇蹟系列 第 4

Go 語言搶票煉金術 Day 4 - Go 的併發工具箱 (二):Mutex 與 RWMutex

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250915/20124462YARMcXUyfa.png

Go 語言搶票煉金術:Go 的併發工具箱 (二):Mutex 與 RWMutex

上一篇,我們學會了如何使用 goroutine 來實現併發,並用 WaitGroup 協調它們。

但是呢,單純的併發執行會帶來一個嚴重的問題:多個 goroutine 同時讀寫同一個變數,會導致數據不一致

今天會探討怎麼保護共享記憶體,並介紹 Go 語言的併發工具:MutexRWMutex

Mutex:記憶體安全的「蠻力」手段

當多個 goroutine 同時讀寫同一個記憶體變數時,就會發生競爭條件。
sync.Mutex (互斥鎖) 是解決這個問題的工具。

它像一扇只有一把鑰匙的門。
任何 goroutine 想要進入被保護的程式碼區塊(臨界區),都必須先獲得鎖 (Lock)。
執行完畢後,必須釋放鎖 (Unlock),下一個等待的 goroutine 才能進入。

// 位於 Day3/mutex/main.go
var (
	counter int
	mu      sync.Mutex // 保護 counter 的鎖
)

func safeIncrement(wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 1000; i++ {
        mu.Lock()   // 取得鎖
        counter++   // --- 臨界區開始 ---
        mu.Unlock() // --- 臨界區結束 --- 釋放鎖
    }
}

這確保了 counter++ 操作的原子性,最後的準確結果。
https://ithelp.ithome.com.tw/upload/images/20250917/201244620klnl1Mn1l.png

https://ithelp.ithome.com.tw/upload/images/20250917/20124462NKOrdjBD4A.png

Mutex 的代價

Mutex 雖然能保證資料安全,但它是有成本的。
它透過強制序列化來解決衝突——一次只允許一個 goroutine 進入臨界區。

在高競爭的場景下,這會導致大量的 goroutine 阻塞和等待,從而嚴重影響程式的並行效能。
它犧牲了並行性來換取安全性。

把它看作是一種必要的惡,一種在沒有更好選擇時才使用的「蠻力」手段。

https://ithelp.ithome.com.tw/upload/images/20250918/20124462K5mLIQW3gv.png

安全的併發處理

到目前為止,我們已經有了 goroutineWaitGroupMutex 三個獨立的工具。

現在將它們組合起來而且解決一個實際問題:如何構建一個允許多個 goroutine 同時操作,但結果依然準確的計數器?

這裡也會用到封裝 (Encapsulation) 的思想。
把你「需要保護的資料 value」 和「用來保護它的鎖mu」 包在同一個結構體 (struct) 裡面。

這樣任何外部程式碼都不能直接觸碰 value。你必須透過我們定義好的方法 (Increment, Value) 來存取它,而這些方法內部已經幫你處理好了加鎖和解鎖的邏輯。

這極大地降低了出錯的風險。你不可能會忘記加鎖,因為你根本沒有機會直接操作資料。

goroutine, WaitGroup, Mutex 的協同工作

// 位於 Day3/example/main.go
package main

import (
	"fmt"
	"sync"
)

// SafeCounter 是一個併發安全的計數器
// 它「封裝」了計數值和一個互斥鎖
type SafeCounter struct {
	mu    sync.Mutex
	value int
}

// Increment 會安全地對計數器加一
func (c *SafeCounter) Increment() {
	// Mutex: 在修改 value 前,鎖定計數器,防止其他 goroutine 介入
	c.mu.Lock()
	defer c.mu.Unlock()
	c.value++
}

// Value 會安全地讀取計數器的值
func (c *SafeCounter) Value() int {
	// Mutex: 即使是讀取,也要鎖定,以防止讀到一個正在被修改的「髒」資料
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.value
}

// worker 代表一個獨立的併發任務
func worker(id int, counter *SafeCounter, wg *sync.WaitGroup) {
	// WaitGroup: 確保在 worker 函式結束時,通知 WaitGroup 任務已完成
	defer wg.Done()

	for i := 0; i < 100; i++ {
		counter.Increment()
	}
	fmt.Printf("Worker %d 完成了 100 次計數\n", id)
}

func main() {
    fmt.Println("=== 三元組組合技展示 ===")
 
	counter := &SafeCounter{}
	var wg sync.WaitGroup

	// 我們要啟動 10 個併發任務
	numWorkers := 10

	for i := 1; i <= numWorkers; i++ {
		// WaitGroup: 每啟動一個 worker,計數器就加 1
		wg.Add(1)

		// goroutine: 使用 'go' 關鍵字,讓每個 worker 在獨立的 goroutine 中併發執行
		go worker(i, counter, &wg)
	}
	// WaitGroup: 阻塞主程式,直到所有 worker 都呼叫了 Done(),計數器歸零
	wg.Wait()
 
    fmt.Printf("最終計數結果:%d\n", counter.Value())
    fmt.Printf("預期結果:1000(10個worker × 100次)\n")
}

執行結果:
https://ithelp.ithome.com.tw/upload/images/20250917/20124462CA25eN990T.png

goroutine, WaitGroup, Mutex 的協同工作
https://ithelp.ithome.com.tw/upload/images/20250918/201244621X7D4rUt4P.png

讀多寫少的救星:sync.RWMutex

雖然 Mutex 很好用,但它有一個限制:即使是讀取操作,也必須等待寫入鎖釋放
讀取頻繁但寫入較少的場景下,Mutex 的效能瓶頸會非常明顯。

這時,我們可以使用更精細的工具:sync.RWMutex (讀寫鎖)

  • RLock(): 取得讀取鎖,允許多個 goroutine 同時讀取。

  • RUnlock(): 釋放讀取鎖。

  • Lock(): 取得寫入鎖,會阻塞所有讀取和寫入操作。

  • Unlock(): 釋放寫入鎖。

使用 RWMutex 重構 SafeCounterValue 方法:

// SafeCounterRWMutex 是一個使用讀寫鎖的併發安全計數器
type SafeCounterRWMutex struct {
	mu      sync.RWMutex
	value   int
}

// Increment 依然使用寫入鎖,確保修改時沒有其他操作
func (c *SafeCounterRWMutex) Increment() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.value++
}

// Value 使用讀取鎖,允許多個讀取同時發生
func (c *SafeCounterRWMutex) Value() int {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.value
}

這個小小的改變,卻能大幅提升程式在讀多寫少場景下的並行效能。
https://ithelp.ithome.com.tw/upload/images/20250918/20124462pX9bOp89nI.png

重點摘要

如何保護共享記憶體:

  • Mutex 是保護共享記憶體的「蠻力」工具,它透過犧牲並行性來保證資料一致性。

  • RWMutexMutex 的進階版,能優化讀多寫少場景的效能。

  • 封裝:將資料和其對應的鎖封裝在一個結構體內,是推薦的實踐方式。

Mutex 是解決競爭條件的基礎手段,但透過鎖來保護共享資源的方式有效能上的代價。

這讓我們去尋找更優雅的方案,而 Go 語言更推崇另一種併發模型——Channel。下篇將繼續探討。

參考資源


上一篇
Go 語言搶票煉金術 Day 3 - Go 的併發工具 (一):goroutine 與 WaitGroup
下一篇
Go 語言搶票煉金術 Day 5 - Go 的併發工具箱 (三):Channel 的消息傳遞
系列文
Go 語言搶票煉金術:解鎖千萬級併發下的原子交易奇蹟5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言