Go語言標準庫學習之sync一(go語言中的鎖)

在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的注意事項

  1. 我們不能使用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
  1. 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報錯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章