Go基礎之鎖的初識
當我們的程式就一個執行緒的時候是不需要用到鎖的,但是通常我們實際的程式碼不會是單個執行緒的,所有這個時候就需要用到鎖了,那麼關於鎖的使用場景主要涉及到哪些呢?
- 當我們多個執行緒在讀相同的資料的時候則是需要加鎖的
- 當我們的程式既有讀又有寫的時候更是需要加鎖的
- 當我們有多個執行緒在寫的時候同樣也是需要加鎖
互斥鎖
互斥鎖:同一個時刻只有一個執行緒能夠拿到鎖
我們先通過一個例子來演示,如果當多個執行緒同時更改一個變數,結果會是怎麼樣
不加鎖版本
package main import ( "sync" "fmt" ) var ( //lock sync.Mutex count int w sync.WaitGroup //用於等待子執行緒執行完之後退出 ) func main() { w.Add(1) // 在呼叫執行緒前執行w.add go func(){ for i:=0;i<100000;i++{ count++ } w.Done() //執行完 執行w.Done }() for i :=0;i<100000;i++{ count++ } w.Wait() // 最後執行w.wait等待所有的執行緒執行完畢 fmt.Println(count) }
當我們執行多次就可以發現,最後的結果基本不可能是我們先看到的:200000
我們修改程式碼程式碼需要加鎖保護的地方加上鎖,並且這裡加的是互斥鎖,修改後的程式碼為:
package main import ( "sync" "fmt" ) var ( lock sync.Mutex count int w sync.WaitGroup //用於等待子執行緒執行完之後退出 ) func main() { w.Add(1) // 在呼叫執行緒前執行w.add go func(){ for i:=0;i<100000;i++{ lock.Lock() count++ lock.Unlock() } w.Done() //執行完 執行w.Done }() for i :=0;i<100000;i++{ lock.Lock() count++ lock.Unlock() } w.Wait() // 最後執行w.wait等待所有的執行緒執行完畢 fmt.Println(count) }
這次當我們多次執行的時候,就能保證我們每次都能看到我們想要的值:200000
接下來看讀寫鎖
讀寫鎖
讀寫鎖主要用到讀多寫少的場景
讀寫鎖分為:讀鎖和寫鎖
如果自己設定了一個寫鎖,那麼其他讀的執行緒以及寫的執行緒都拿不到鎖,這個時候和互斥鎖的功能相同
如果自己設定了一個讀鎖,那麼其他寫的執行緒是拿不到鎖的,但是其他讀的執行緒都是可以拿到這個鎖
我們把上面的例子程式碼進行更改:
package main import ( "sync" "fmt" ) var ( rwlock sync.RWMutex w sync.WaitGroup count int ) func main() { w.Add(1) go func(){ for i:=0;i<1000000;i++{ rwlock.Lock() // 這裡定義了一個寫鎖 count++ rwlock.Unlock() } w.Done() }() for i:=0;i<1000000;i++{ rwlock.Lock() // 這裡定義了一個寫鎖 count++ rwlock.Unlock() } w.Wait() fmt.Println(count) }
通過設定寫鎖,我們同樣可以實現資料的一致性
下面是一個讀鎖的使用例子:
package main
import (
"sync"
"fmt"
)
var (
rwlock sync.RWMutex
w sync.WaitGroup
count int
)
func main() {
w.Add(1)
go func(){
for i:=0;i<1000000;i++{
rwlock.Lock() // 這裡定義了一個寫鎖
count++
rwlock.Unlock()
}
w.Done()
}()
for i:=0;i<16;i++{
w.Add(1)
go func(){
rwlock.RLock() //這裡定義了一個讀鎖
fmt.Println(count)
rwlock.RUnlock() //釋放讀鎖
w.Done()
}()
}
w.Wait()
fmt.Println(count)
}
Go中的原子操作
原子操作,我們則不需加鎖,也能保證資料的一致性
並且如果只是計算,那麼原子操作則是最快的
例項程式碼:
package main
import (
"sync"
//"time"
"sync/atomic"
"fmt"
)
var (
w sync.WaitGroup
count int32
)
func main() {
w.Add(1)
//start := time.Now().UnixNano()
go func() {
for i:=0;i<1000000;i++{
atomic.AddInt32(&count,1)
}
w.Done()
}()
for i:=0;i<1000000;i++{
atomic.AddInt32(&count,1)
}
w.Wait()
//end := time.Now().UnixNano()
//fmt.Println((end- start)/1000/1000)
fmt.Println(count)
}
關於互斥鎖的補充
互斥鎖需要注意的問題:
1、不要重複鎖定互斥鎖
2、不要忘記解鎖互斥鎖, 必要時使用defer語句
3、不要對尚未鎖定或者已解鎖的互斥鎖解鎖
4、不要對在多個函式之間直接傳遞互斥鎖
對已經鎖定的互斥鎖進行鎖定,會立即阻塞當前的goroutine 這個goroutine所執行的流程會一直停滯在該呼叫互斥鎖的Lock方法的那行程式碼
所謂死鎖: 當前程式中的主goroutine以及我們啟用的那些goroutine 都已經被阻塞,這些goroutine可以被稱為使用者級的goroutine 這就相當於整個程式已經停滯不前了,並且這個時候go程式會丟擲如下的panic:
fatal error: all goroutines are asleep - deadlock!
並且go語言執行時系統丟擲自行丟擲的panic都屬於致命性錯誤,都是無法被恢復的,呼叫recover函式對他們起不到任何作用
Go語言中的互斥鎖是開箱即用的,也就是我們宣告一個sync.Mutex 型別的變數,就可以直接使用它了,需要注意:該型別是一個結構體型別,屬於值型別的一種,把它傳給一個函式將它從函式中返回,把它賦值給其他變數,讓它進入某個通道都會導致他的副本的產生。並且原值和副本以及多個副本之間是完全獨立的,他們都是不同的互斥鎖,所以不應該將鎖通過函式的引數進行傳遞
關於讀寫鎖的補充
1、在寫鎖已被鎖定的情況下再次試圖鎖定寫鎖,會阻塞當前的goroutine
2、在寫鎖已被鎖定的情況下再次試圖鎖定讀鎖,也會阻塞當前的goroutine
3、在讀鎖已被鎖定的情況下試圖鎖定寫鎖,同樣會阻塞當前的goroutine
4、在讀鎖已被鎖定的情況下再試圖鎖定讀鎖,並不會阻塞當前的goroutine
對於某個受到讀寫鎖保護的共享資源,多個寫操作不能同時進行,寫操作和讀操作也不能同時進行,但多個讀操作卻可以同時進行
對寫鎖進行解鎖,會喚醒“所有因試圖鎖定讀鎖,而被阻塞的goroutine”, 並且這個通常會使他們都成功完成對讀鎖的鎖定
對讀鎖進行解鎖,只會在沒有其他讀鎖鎖定的前提下,喚醒“因試圖鎖定寫鎖,而被阻塞的goroutine” 並且只會有一個被喚醒的goroutine能夠成功完成對寫鎖的鎖定,其他的goroutine
還要在原處繼續等待,至於哪一個goroutine,那麼就要看誰等待的事件最長
解鎖讀寫鎖中未被鎖定的寫鎖, 會立即引發panic ,對其中的讀鎖也是如此,並且同樣是不可恢復的