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 后,釋放鎖
程式退出...
系列導讀
01. 開發環境的搭建(Goland & VS Code)
02. 學習五種變數創建的方法
03. 詳解資料型別:****整形與浮點型
04. 詳解資料型別:byte、rune與string
05. 詳解資料型別:陣列與切片
06. 詳解資料型別:字典與布爾型別
07. 詳解資料型別:指標
08. 面向物件編程:結構體與繼承
09. 一篇文章理解 Go 里的函式
10. Go語言流程控制:if-else 條件陳述句
11. Go語言流程控制:switch-case 選擇陳述句
12. Go語言流程控制:for 回圈陳述句
13. Go語言流程控制:goto 無條件跳轉
14. Go語言流程控制:defer 延遲呼叫
15. 面向物件編程:介面與多型
16. 關鍵字:make 和 new 的區別?
17. 一篇文章理解 Go 里的陳述句塊與作用域
18. 學習 Go 協程:goroutine
19. 學習 Go 協程:詳解信道/通道
20. 幾個信道死鎖經典錯誤案例詳解
21. 學習 Go 協程:WaitGroup
22. 學習 Go 協程:互斥鎖和讀寫鎖
23. Go 里的例外處理:panic 和 recover
24. 超詳細解讀 Go Modules 前世今生及入門使用
25. Go 語言中關于包匯入必學的 8 個知識點
26. 如何開源自己寫的模塊給別人用?
27. 說說 Go 語言中的型別斷言?
28. 這五點帶你理解Go語言的select用法

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/6566.html
標籤:Go
下一篇:Go檔案操作
