Goroutine 的同步(第三部分)
- 第一部分:https://studygolang.com/articles/14118
- 第二部分:https://studygolang.com/articles/14478
mutex 和 sync.Once 介紹
假設你的程式中有一個需要某種初始化的功能。該 Bootstrap 程式成本很高,因此將其推遲到實際使用功能的那一刻是有意義的。這樣,當功能未啟用時,就不會浪費 CPU 週期。 這在 Go 中如何完成?
package main import "fmt" var capitals map[string]string func Bootstrap() { capitals = make(map[string]string) capitals["France"] = "Paris" capitals["Germany"] = "Berlin" capitals["Japan"] = "Tokyo" capitals["Brazil"] = "Brasilia" capitals["China"] = "Beijing" capitals["USA"] = "Washington" ... capitals["Poland"] = "Warsaw" } func getCapitalCity(country string) string { if capitals == nil { Bootstrap() } return capitals[country] } func main() { fmt.Println(getCapitalCity("Poland")) fmt.Println(getCapitalCity("USA")) fmt.Println(getCapitalCity("Japan")) }
你可以想象,如果它可以處理所有的國家,其他類似資料庫的結構,使用 I/O 操作等,則 bootstrap 函式可能非常昂貴。上述解決方案看起來簡單且優雅,但不幸的是,它並不會正常工作。問題在於當 bootstrap 函式在執行時無法阻止其它 Goroutine 做同樣的事。而這些繁重的計算只做一次是很可取的。另外在 capitals 剛剛被初始化而其中的 key 還未設定時,其它 Goroutine 會看到它不為 nil 從而嘗試從空的 map 中獲取值。
ofollow,noindex" target="_blank">sync.Mutex
Go 有包含很多好東西的內建 sync 包。我們可以使用 mutex (mutual exclusion) 來解決我們的同步問題。
import ( "fmt" "sync" ) ... var ( capitals map[string]string mutex sync.Mutex ) ... func getCapitalCity(country string) string { mutex.Lock() if capitals == nil { Bootstrap() } mutex.Unlock() return capitals[country] }
bootstrap 程式可被多次執行的問題解決了。如果任何一個 Goroutine 正在執行 bootstrap 或者甚至在判斷 capitals == nil
,那麼其它 Goroutine 會在 mutex.Lock()
處等待。 Unlock 函式一結束另一個 Goroutine 就會被“准許進入”。
但是一次只能有一個 Goroutine 執行被放在 mutex.Lock()
和 mutex.Unlock()
之間的程式碼。因此如果存放首都城市的 map 被多個 Goroutine 讀取,那麼一切都會在 if 語句處被處理成一個接一個的。從根本上說對於 map 的讀操作(包括判斷它是否為 nil )應該允許一次處理多個,因為它是執行緒安全的。
sync.RWMutex
讀 / 寫 mutex 可以同時被多個 reader 或者一個 writer 持有(writer 是指改寫資料的某種東西):
mutex sync.RWMutex ... func getCapitalCity(country string) string { mutex.RLock() if capitals != nil { country := capitals[country] mutex.RUnlock() return country } mutex.RUnlock() mutex.Lock() if capitals == nil { Bootstrap() } mutex.Unlock() return getCapitalCity(country) }
現在程式碼變得複雜多了。第一部分使用讀鎖以允許多個 reader 同時讀取 capitals 。在一開始 Bootstrap 還未完成,所以呼叫者會拿到 mutex.Lock()
並且做必要的初始化。當這部分結束,函式可以被再次呼叫來獲取期望的值。這些都在 Bootstrap 函式已經返回之後。
最新的解決方法很顯然維護起來比較難。幸運的是有一個內建的方法恰好幫我們應對這種情況……
sync.Once
once sync.Once ... func getCapitalCity(country string) string { once.Do(bootstrap) return capital[country] }
上面的程式碼比第一個簡明版的解決方法(它並不正常工作)更加簡單。可以保證 bootstrap 只會被呼叫一次,並且從 map 中讀資料僅在 bootstrap 返回後才會被執行。
點贊以幫助別人發現這篇文章。如果你想得到新文章的更新,請關注我。
資源
The Go memory model specifies the conditions under which reads of a variable in one Goroutine can be guaranteed to …
golang.org
Package sync provides basic synchronization primitives such as mutual exclusion locks. Other than the Once and …
golang.org
Golang Programming Concurrency Synchronization Software Development
喜歡讀嗎?給 Michał Łowicki 一些掌聲吧。
簡單鼓勵下還是大喝采,根據你對這篇文章的喜歡程度鼓掌吧。