在go語言多線程編程的過程中,我們會遇到多線程進行資源讀寫的問題,在GO語言中我們可以使用channel進行控制,但是除了channel我們還可以通過sync庫進行資源的讀寫控制,這也就是我們常說的鎖。鎖的作用就是某個協程(線程)在訪問某個資源時先鎖住,防止其它協程的訪問,等訪問完畢解鎖後其他協程再來加鎖進行訪問。本文向記錄了學習sync標準庫的學習筆記,希望對你有幫助。
一、互斥鎖
1.什麼是互斥鎖
這裏摘錄百度百科的解釋 :
互斥鎖是用來保證共享數據操作的完整性的。每個對象都對應於一個可稱爲" 互斥鎖" 的標記,這個標記用來保證在任一時刻,只能有一個線程訪問該對象。
2.sync.Mutex
在sync庫中Mutex對象實現了兩個方法,Lock和UnLock,從字面意思就可以理解,一個是鎖另一個是釋放鎖。
// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
type Mutex struct {
state int32
sema uint32
}
在源碼定義中我們可以看出,Mutex在使用後不能被複制,因此這裏我們要注意。
3.互斥鎖的使用
接下來我們看一下如何使用Mutex進行資源鎖定和釋放。
package main
import (
"fmt"
"sync"
"time"
)
// 定義一個鎖
var m = new(sync.Mutex)
func StdOut(s string) {
// 創建一個互斥鎖
//m := new(sync.Mutex)
m.Lock()
// 當main函數執行完成後,釋放鎖
defer m.Unlock()
for _, data := range s {
fmt.Printf("%c", data)
}
fmt.Println()
}
func Person1(s string) {
StdOut(s)
}
func main() {
go Person1("Random_w1")
go Person1("Random_w2")
Person1("Random_w3")
// 等待兩秒,讓goroutine運行完成
time.Sleep(time.Millisecond * 100)
}
Output:
$ go run main.go
Random_w3
Random_w1
Random_w2
如果將StdOut中的Lock刪除掉,那麼輸出就會混亂:
$ go run main.go
RandomRandom_w1
Random_w2
_w3
通過比對兩種情況大家應該理解了互斥鎖的使用了。
二、讀寫鎖
1.什麼是讀寫鎖
同樣這裏我引用百度百科的解釋:
讀寫鎖實際是一種特殊的自旋鎖,它把對共享資源的訪問者劃分成讀者和寫者,讀者只對共享資源進行讀訪問,寫者則需要對共享資源進行寫操作。
互斥鎖的本質是當一個goroutine訪問的時候,其他goroutine都不能訪問。這樣在資源同步,避免競爭的同時也降低了程序的併發性能。程序由原來的並行執行變成了串行執行。
2. sync.RWMutex
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount int32 // number of pending readers
readerWait int32 // number of departing readers
}
sync.RWMutex 結構體實現了五種方法:
- func (rw *RWMutex) Lock() 寫鎖定
- func (rw *RWMutex) UnLock() 寫解鎖
- func (rw *RWMutex) RLock() 讀鎖定
- func (rw *RWMutex) RUnLock() 讀解鎖
- func (rw *RWMutex) RLocker() Locker
RWMutex的使用主要事項
- 讀鎖的時候無需等待讀鎖的結束
- 讀鎖的時候要等待寫鎖的結束
- 寫鎖的時候要等待讀鎖的結束
- 寫鎖的時候要等待寫鎖的結束
3. 讀寫鎖的使用
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// 定義一個鎖
var m = new(sync.RWMutex)
var count int
// Write 對count進行寫操作
func Write(n int) {
rand.Seed(time.Now().UnixNano())
fmt.Printf("寫 goroutine %d 正在寫數據...\n", n)
m.Lock()
num := rand.Intn(500)
count = num
fmt.Printf("寫 goroutine %d 寫數據結束,寫入新值 %d\n", n, num)
m.Unlock()
}
// Read 對count進行讀操作
func Read(n int) {
m.RLock()
fmt.Printf("讀 goroutine %d 正在讀取數據...\n", n)
num := count
fmt.Printf("讀 goroutine %d 讀取數據結束,讀到 %d\n", n, num)
m.RUnlock()
}
func main() {
// 創建goroutine,進行讀寫操作
for i := 0; i < 3; i++ {
go Read(i)
go Write(i)
}
time.Sleep(time.Second)
}
Output:
$ go run main.go
讀 goroutine 1 正在讀取數據...
讀 goroutine 1 讀取數據結束,讀到 0
讀 goroutine 0 正在讀取數據...
讀 goroutine 0 讀取數據結束,讀到 0
寫 goroutine 2 正在寫數據...
寫 goroutine 0 正在寫數據...
寫 goroutine 1 正在寫數據...
讀 goroutine 2 正在讀取數據...
讀 goroutine 2 讀取數據結束,讀到 0
寫 goroutine 2 寫數據結束,寫入新值 337
寫 goroutine 0 寫數據結束,寫入新值 16
寫 goroutine 1 寫數據結束,寫入新值 134
從Output中我們可以看到,當讀鎖被鎖定時,寫鎖時阻塞狀態,只有當讀鎖解除後,count才能寫入新值。
三、Cond的使用
Cond是一個比較冷門的結構體,sync.Cond用於goroutine之間的協作,用於協程的掛起和喚醒。
1. Cond結構體
從下面的結構體我麼可以看出Cond在被創建後是不能複製的,和互斥鎖類似。
// A Cond must not be copied after first use.
type Cond struct {
noCopy noCopy // noCopy可以嵌入到結構中,在第一次使用後不可複製,使用go vet作爲檢測使用
L Locker // 根據需求初始化不同的鎖,如*Mutex 和 *RWMutex
notify notifyList // 通知列表,調用Wait()方法的goroutine會被放入list中,每次喚醒,從這裏取出
checker copyChecker // 複製檢查,檢查cond實例是否被複制
}
2. Cond結構體實現的方法
Cond結構體實現了四個方法,分別是:
func (c *Cond) Wait()
必須獲取該鎖之後才能調用Wait()方法,Wait方法在調用時會釋放底層鎖Locker,並且將當前goroutine掛起,直到另一個goroutine執行Signal或者Broadcase,該goroutine纔有機會重新喚醒,並嘗試獲取Locker,完成後續邏輯。也就是在等待被喚醒的過程中是不佔用鎖Locker的,這樣就可以有多個goroutine可以同時處於Wait(等待被喚醒的狀態)func (c *Cond) Signal()
喚醒等待隊列中的一個goroutine,一般都是任意喚醒隊列中的一個goroutine。func (c *Cond) Broadcast()
喚醒等待隊列中的所有goroutine。func NewCond(l Locker) *Cond
,使用Locker創建一個Cond對象。
3. Cond的使用
下面的示例代碼中我們使用cond.Wait讓goroutine進入等待狀態,在main函數中,我們分別測試了使用Siginal和Broadcast將goroutine喚醒,爲了表示Siginal一次只能喚醒一個goroutine因此加入了時間,正常情況下實例中的goroutine在不到一微秒的時間就可以執行完成,但是我們延時了一秒,除了被喚醒的goroutine運行外,其他goroutine並沒有執行。使用Broadcast我們可以看到剩下的兩個goroutine快速執行完成。
package main
import (
"fmt"
"sync"
"time"
)
// 定義一個鎖
var mutex = new(sync.Mutex)
// 初始化一個cond
var cond = sync.NewCond(mutex)
func CondTest() {
// 5個goroutine正常情況下一微秒時間都可以運行完
for i := 0; i < 5; i++ {
id := i
go func() {
mutex.Lock()
defer mutex.Unlock()
// 讓所有goroutine等待
cond.Wait()
fmt.Printf("goroutine %d 運行完成\n", id)
}()
}
}
// 輸出時間
func PrintTime() {
fmt.Println(time.Now().Format("15:04:05"))
}
func main() {
CondTest()
// 運行三個goroutine
for i := 0; i < 3; i++ {
PrintTime()
cond.Signal()
time.Sleep(time.Second)
}
// 通過Broadcast喚醒所有的goroutine
PrintTime()
cond.Broadcast()
time.Sleep(time.Millisecond)
}
Output:
$ go run main.go
14:40:29
goroutine 1 運行完成
14:40:30
goroutine 0 運行完成
14:40:31
goroutine 2 運行完成
14:40:32
goroutine 4 運行完成
goroutine 3 運行完成
四、更優雅的等待goroutine結束(sync.WaitGroup)
平時我們在測試或者創建goroutine後,往往通過延時的方式等待goroutine退出,這種方式是比較耗費時間的,在sync中我們可以使用WaitGroup的方法更優雅的等待goroutine結束。
1. WaitGroup的使用
package main
import (
"fmt"
"sync"
"time"
)
func main() {
// 新建一個WaitGroup對象
wg := sync.WaitGroup{}
// WaitGroup的數量爲3
wg.Add(3)
for i := 0; i < 3; i++ {
id := i
go func() {
fmt.Printf("goroutine %d 運行完成\n", id)
// goroutine運行完成,通知wg
wg.Done()
}()
}
//wg程序阻塞,等待所有goroutine運行完成
wg.Wait()
}
Output:
$ go run main.go
goroutine 2 運行完成
goroutine 0 運行完成
goroutine 1 運行完成
2. WaitGroup的注意事項
- 我們不能使用Add() 給wg 設置一個負值,否則代碼將會報錯。
$ go run main.go
panic: sync: negative WaitGroup counter
goroutine 1 [running]:
sync.(*WaitGroup).Add(0xc000070070, 0xffffffffffffffff)
c:/Go/src/sync/waitgroup.go:74 +0x13c
main.main()
D:/GOCODE/Test/main.go:12 +0x54
exit status 2
- WaitGroup對象不是一個引用類型,在通過函數傳值的時候需要使用地址。
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func main() {
wg := sync.WaitGroup{}
wg.Add(3)
for i := 0; i < 3; i++ {
id := i
go func(wg *sync.WaitGroup) {
fmt.Printf("goroutine %d 運行完成\n", id)
wg.Done()
}(&wg)
}
wg.Wait()
}
Output:
$ go run main.go
goroutine 2 運行完成
goroutine 0 運行完成
goroutine 1 運行完成
上面的main函數如果改成這樣:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func main() {
wg := sync.WaitGroup{}
wg.Add(3)
for i := 0; i < 3; i++ {
id := i
go func(wg sync.WaitGroup) {
fmt.Printf("goroutine %d 運行完成\n", id)
wg.Done()
}(wg)
}
wg.Wait()
}
Output:
$ go run main.go
goroutine 2 運行完成
goroutine 0 運行完成
goroutine 1 運行完成
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000070078)
c:/Go/src/runtime/sema.go:56 +0x40
sync.(*WaitGroup).Wait(0xc000070070)
c:/Go/src/sync/waitgroup.go:130 +0x6c
main.main()
D:/GOCODE/Test/main.go:20 +0xbc
exit status 2
可以看到,不使用地址傳遞參數會在goroutine運行完成之後觸發panic報錯。