Hi,大家好,我是明哥。
在自己學習 Golang 的這段時間里,我寫了詳細的學習筆記放在我的個人微信公眾號 《Go編程時光》,對於 Go 語言,我也算是個初學者,因此寫的東西應該會比較適合剛接觸的同學,如果你也是剛學習 Go 語言,不防關注一下,一起學習,一起成長。
我的在線博客:http://golang.iswbm.com
我的 Github:github.com/iswbm/GolangCodingTime
在 「19. 學習 Go 協程:詳解信道/通道」這一節里我詳細地介紹信道的一些用法,要知道的是在 Go 語言中,信道的地位非常高,它是 first class 級別的,面對並發問題,我們始終應該優先考慮使用信道,如果通過信道解決不了的,不得不使用共享內存來實現並發編程的,那 Golang 中的鎖機制,就是你繞不過的知識點了。
今天就來講一講 Golang 中的鎖機制。
在 Golang 里有專門的方法來實現鎖,還是上一節里介紹的 sync 包。
這個包有兩個很重要的鎖類型
一個叫 Mutex, 利用它可以實現互斥鎖。
一個叫 RWMutex,利用它可以實現讀寫鎖。
1. 互斥鎖 :Mutex
使用互斥鎖(Mutex,全稱 mutual exclusion)是為了來保護一個資源不會因為並發操作而引起沖突導致數據不准確。
舉個例子,就像下面這段代碼,我開啟了三個協程,每個協程分別往 count 這個變量加1000次 1,理論上看,最終的 count 值應試為 3000
package main
import (
"fmt"
"sync"
)
func add(count *int, wg *sync.WaitGroup) {
for i := 0; i < 1000; i++ {
*count = *count + 1
}
wg.Done()
}
func main() {
var wg sync.WaitGroup
count := 0
wg.Add(3)
go add(&count, &wg)
go add(&count, &wg)
go add(&count, &wg)
wg.Wait()
fmt.Println("count 的值為:", count)
}
可運行多次的結果,都不相同
// 第一次
count 的值為: 2854
// 第二次
count 的值為: 2673
// 第三次
count 的值為: 2840
原因就在於這三個協程在執行時,先讀取 count 再更新 count 的值,而這個過程並不具備原子性,所以導致了數據的不准確。
解決這個問題的方法,就是給 add 這個函數加上 Mutex 互斥鎖,要求同一時刻,僅能有一個協程能對 count 操作。
在寫代碼前,先了解一下 Mutex 鎖的兩種定義方法
// 第一種
var lock *sync.Mutex
lock = new(sync.Mutex)
// 第二種
lock := &sync.Mutex{}
然后就可以修改你上面的代碼,如下所示
import (
"fmt"
"sync"
)
func add(count *int, wg *sync.WaitGroup, lock *sync.Mutex) {
for i := 0; i < 1000; i++ {
lock.Lock()
*count = *count + 1
lock.Unlock()
}
wg.Done()
}
func main() {
var wg sync.WaitGroup
lock := &sync.Mutex{}
count := 0
wg.Add(3)
go add(&count, &wg, lock)
go add(&count, &wg, lock)
go add(&count, &wg, lock)
wg.Wait()
fmt.Println("count 的值為:", count)
}
此時,不管你執行多少次,輸出都只有一個結果
count 的值為: 3000
使用 Mutext 鎖雖然很簡單,但仍然有幾點需要注意:
- 同一協程里,不要在尚未解鎖時再次使加鎖
- 同一協程里,不要對已解鎖的鎖再次解鎖
- 加了鎖后,別忘了解鎖,必要時使用 defer 語句
3. 讀寫鎖:RWMutex
Mutex 是最簡單的一種鎖類型,他提供了一個傻瓜式的操作,加鎖解鎖加鎖解鎖,讓你不需要再考慮其他的。
簡單同時意味着在某些特殊情況下有可能會造成時間上的浪費,導致程序性能低下。
舉個例子,我們平時去圖書館,要嘛是去借書,要嘛去還書,借書的流程繁鎖,沒有辦卡的還要讓管理員給你辦卡,因此借書通常都要排老長的隊,假設圖書館里只有一個管理員,按照 Mutex(互斥鎖)的思想, 這個管理員同一時刻只能服務一個人,這就意味着,還書的也要跟借書的一起排隊。
可還書的步驟非常簡單,可能就把書給管理員掃下碼就可以走了。
如果讓還書的人,跟借書的人一起排隊,那估計有很多人都不樂意了。
因此,圖書館為了提高整個流程的效率,就允許還書的人,不需要排隊,可以直接自助還書。
圖書管將館里的人分得更細了,對於讀者的不同需求提供了不同的方案。提高了效率。
RWMutex,也是如此,它將程序對資源的訪問分為讀操作和寫操作
- 為了保證數據的安全,它規定了當有人還在讀取數據(即讀鎖占用)時,不允計有人更新這個數據(即寫鎖會阻塞)
- 為了保證程序的效率,多個人(線程)讀取數據(擁有讀鎖)時,互不影響不會造成阻塞,它不會像 Mutex 那樣只允許有一個人(線程)讀取同一個數據。
理解了這個后,再來看看,如何使用 RWMutex?
定義一個 RWMuteux 鎖,有兩種方法
// 第一種
var lock *sync.RWMutex
lock = new(sync.RWMutex)
// 第二種
lock := &sync.RWMutex{}
RWMutex 里提供了兩種鎖,每種鎖分別對應兩個方法,為了避免死鎖,兩個方法應成對出現,必要時請使用 defer。
- 讀鎖:調用 RLock 方法開啟鎖,調用 RUnlock 釋放鎖
- 寫鎖:調用 Lock 方法開啟鎖,調用 Unlock 釋放鎖(和 Mutex類似)
接下來,直接看一下例子吧
package main
import (
"fmt"
"sync"
"time"
)
func main() {
lock := &sync.RWMutex{}
lock.Lock()
for i := 0; i < 4; i++ {
go func(i int) {
fmt.Printf("第 %d 個協程准備開始... \n", i)
lock.RLock()
fmt.Printf("第 %d 個協程獲得讀鎖, sleep 1s 后,釋放鎖\n", i)
time.Sleep(time.Second)
lock.RUnlock()
}(i)
}
time.Sleep(time.Second * 2)
fmt.Println("准備釋放寫鎖,讀鎖不再阻塞")
// 寫鎖一釋放,讀鎖就自由了
lock.Unlock()
// 由於會等到讀鎖全部釋放,才能獲得寫鎖
// 因為這里一定會在上面 4 個協程全部完成才能往下走
lock.Lock()
fmt.Println("程序退出...")
lock.Unlock()
}
輸出如下
第 1 個協程准備開始...
第 0 個協程准備開始...
第 3 個協程准備開始...
第 2 個協程准備開始...
准備釋放寫鎖,讀鎖不再阻塞
第 2 個協程獲得讀鎖, sleep 1s 后,釋放鎖
第 3 個協程獲得讀鎖, sleep 1s 后,釋放鎖
第 1 個協程獲得讀鎖, sleep 1s 后,釋放鎖
第 0 個協程獲得讀鎖, sleep 1s 后,釋放鎖
程序退出...
系列導讀
24. 超詳細解讀 Go Modules 前世今生及入門使用

