並發安全(競態問題)
讓一個程序並發安全並不需要其中的每一個具體類型都是並發安全的。實際上並發安全的類型其實是特例而不是普遍存在的,所以僅在文檔指出類型是安全的情況下,才可以並發的訪問一個變量。與之對應的是,導出的包級別函數通常可以認為是並發安全的。因為包級別的變量無法限制在一個goroutine內。所以那些修改這些變量的函數必須采用互斥機制。
例如下面代碼就會存在競態問題導致結果與與其不否
var x int64 var wg sync.WaitGroup func add() { for i := 0; i < 5000; i++ { x = x + 1 } wg.Done() } func main() { wg.Add(2) go add() go add() wg.Wait() fmt.Println(x) }
這時引入鎖至關重要,在go中sync包提供了鎖機制
sync包
sync包中主要有:Locker, Cond, Map, Mutex, Once, Pool,、RWMutex, WaitGroup
互斥鎖:sync.Mutex
互斥鎖的模式應用非常廣泛,所以sync包有一個單獨的Mutex類型來支持這種模式,它的Lock方法用來獲取令牌(token,此過程也稱為上鎖), Unlock方法用於釋放令牌。
var x int64 var wg sync.WaitGroup var lock sync.Mutex func add() { for i := 0; i < 5000; i++ { lock.Lock() // 加鎖 x = x + 1 lock.Unlock() // 解鎖 } wg.Done() } func main() { wg.Add(2) go add() go add() wg.Wait() fmt.Println(x) }
在Lock和Unlock之間的代碼,可以自由的讀取和修改共享變量,這一部分稱為臨界區域。在鎖的持有人調用Unlock之前,其他的goroutine不能獲取鎖。
讀寫互斥鎖:sync.RWMutex
在某種情況下,函數只須讀取變量的狀態,所以多個函數可以安全的並發執行。只要在寫入沒有同時就行,在這種場景下,就需要一種特殊的安全鎖,它只允許讀操作可以並發執行,但寫操作需要獲得安全獨享的訪問權限。
var ( x int64 wg sync.WaitGroup lock sync.Mutex rwlock sync.RWMutex ) func write() { // lock.Lock() // 加互斥鎖 rwlock.Lock() // 加寫鎖 x = x + 1 time.Sleep(10 * time.Millisecond) // 假設讀操作耗時10毫秒 rwlock.Unlock() // 解寫鎖 // lock.Unlock() // 解互斥鎖 wg.Done() } func read() { // lock.Lock() // 加互斥鎖 rwlock.RLock() // 加讀鎖 time.Sleep(time.Millisecond) // 假設讀操作耗時1毫秒 rwlock.RUnlock() // 解讀鎖 // lock.Unlock() // 解互斥鎖 wg.Done() } func main() { start := time.Now() for i := 0; i < 10; i++ { wg.Add(1) go write() } for i := 0; i < 1000; i++ { wg.Add(1) go read() } wg.Wait() end := time.Now() fmt.Println(end.Sub(start)) }
延遲初始化:sync.Once
延遲是一個昂貴的初始化步驟到有實際需求的時刻是一個很好的實踐。而sync.Once是一個可以被多次調用但是只執行一次,若每次調用Do時傳入參數f不同,但是只有第一個才會被執行。
sync.Once有一個 Do方法。示例如下
var once sync.Once onceBody := func() { fmt.Println("Only once") } done := make(chan bool) for i := 0; i < 10; i++ { go func() { once.Do(onceBody) done <- true }() } for i := 0; i < 10; i++ { <-done }
執行雖然調用了10次,但是只執行了1次。BTW:這個東西可以用來寫單例。
單例(借助Once)
package singleton import ( "sync" ) type singleton struct {} var instance *singleton var once sync.Once func GetInstance() *singleton { once.Do(func() { instance = &singleton{} }) return instance }
sync.WaitGroup
waitgroup 用來等待一組goroutines的結束,在主Goroutine里聲明,並且設置要等待的goroutine的個數,每個goroutine執行完成之后調用 Done,最后在主Goroutines 里Wait即可。waitgroup含有三種方法
func (wg *WaitGroup) Add(d int) //計數器+d func (wg *WaitGroup) Done() //計數器-1 func (wg *WaitGroup) Wait() //阻塞直到計數器變為0
一個簡單的例子
var wg sync.WaitGroup func hello() { defer wg.Done() fmt.Println("Hello Goroutine!") } func main() { wg.Add(1) go hello() // 啟動另外一個goroutine去執行hello函數 fmt.Println("main goroutine done!") wg.Wait() }
競態檢測器(race detector)
在編寫時即使最大的仔細還會出現並發上的錯誤,幸運的是,go語言在運行時和工具鏈裝備了一個精致並易於使用的動態分析工具:競態檢測器。
簡單的把 -race命令行參數加到go build, go run, go test命令里邊即可使用該功能。它會讓編譯器為你的應用或測試構建一個修訂后的版本。
競態檢測器會研究事件流,找到那些有問題的案例,即一個goroutine寫入一個變量后,中間沒有任何同步的操作,就有另一個goroutine寫入了該變量。這種案例表明有對共享變量的並發訪問,即數據動態。
競態檢測器報告所有實際運行了的數據競態。它只能檢測到那些在運行時發生的競態,無法用來保證肯定不會發生京態。
有興趣的可以仔細研究。