首頁 > 軟體

Go中sync 包Cond使用場景分析

2023-03-05 14:01:07

背景

編寫程式碼過程中, 通常有主協程和多個子協程進行共同作業的過程,比如通過 WaitGroup 可以實現當所有子協程完成之後, 主協程再繼續執行, 具體可參考:Go 中goroutine和WaitGroup的使用

如上的場景是主協程等待子協程達到某個狀態再繼續執行。 但是反過來怎麼操作呢,要求一組子協程等待主協達到某個狀態時才繼續執行。這個時候就需要用到 Cond 了

Cond 簡介

Cond 是和某個條件相關,在條件還沒有滿足的時候,所有等待這個條件的協程都會被阻塞住,只有這個條件滿足的時候,等待的協程才可能繼續進行下去。
Cond 在初始化的時候,需要關聯一個 Locker 介面的範例,一般會使用 Mutex 或者 RWMutex。
Cond 關聯的 Locker 範例可以通過 c.L 存取,它內部維護著一個先入先出的等待佇列。
Cond 分別有三個方法

  • Wait

會把當前協程放入Cond的等待佇列中並阻塞,直到被Signal或者Broadcast方法從等待佇列中移除並喚醒,用於子協程阻塞。

  • Signal

主協程喚醒等待佇列中的一個子協程,先喚醒最先阻塞的子協程,被喚醒的子協程繼續執行。

  • Broadcast

主協程喚醒等待佇列中的全部協程,所有子協程繼續執行。

注意:呼叫Signal和Broadcast方法,不強求持有c.L的鎖,呼叫Wait方法是必須要持有c.L的鎖。

使用範例

Signal的使用場景

大家都去醫院先排隊,然後等待叫號,先排隊的先叫號。這次模擬有5個病人,分別先排隊。 然後護士根據排隊先後來叫號;
具體場景是,5個病人在三秒中之內分別排號,護士今天要叫5個號,一秒叫一個,叫完5個號就結束了
程式碼如下:

package main
import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func main() {
	c := sync.NewCond(&sync.Mutex{})
	num := 0
	// 當前叫號是幾號
	hand_num := 0
	for i := 0; i < 5; i++ {
		go func(i int) {
			// 分別在不同時間排隊
			time.Sleep(time.Second * time.Duration(rand.Int63n(10)))
			c.L.Lock()
			num++
			// 當前取得號。
			cur := num
			fmt.Printf("%s  %d 號病人取到了 %d 號n", time.Now().Format("2006-01-02 15:04:05"), i, cur)
			// 取到號了,等待叫號
			c.Wait()
			fmt.Printf("%s  %d 號病人排隊號是 %d 號,被叫號了n", time.Now().Format("2006-01-02 15:04:05"), i, cur)
			hand_num = cur
			c.L.Unlock()
		}(i)
	}

	// 都叫號了
	for hand_num != 5 {
		// 叫號
		c.Signal()
		time.Sleep(time.Second * 1)
	}

	time.Sleep(time.Second * 10)
}

執行結果如下

結果表明,5個病人,分別在三秒鐘內先後取號, 然後護士每過一秒鐘按照排隊的先後順序叫一個號(叫號的過程依然有病人取號),先取號的被先叫號。
此場景中,5個病人相當於5個協程, 主協程反覆使用Signal() 按照順序一個個喚醒阻塞的子協程。

Broadcast的使用場景

場景為如下: 運動員跑步比賽,要求8秒內全部運動員準備好,然後等待教練發令, 教練10秒後發令,所有運動員在發令後開始跑。

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func main() {
	c := sync.NewCond(&sync.Mutex{})

	for i := 0; i < 10; i++ {
		go func(i int) {
			// 隨機一個8秒內的準備時間
			time.Sleep(time.Second * time.Duration(rand.Int63n(8)))
			fmt.Printf("%s 運動員%d已準備就緒n", time.Now().Format("2006-01-02 15:04:05"), i)
			c.L.Lock()
			// 準備完畢,等待教練發令
			c.Wait()
			c.L.Unlock()
			fmt.Printf("%s 運動員%d開跑n", time.Now().Format("2006-01-02 15:04:05"), i)
		}(i)
	}

	// 主協程等待10秒後發令
	time.Sleep(time.Second * 10)
	fmt.Printf("%s 教練發令。n", time.Now().Format("2006-01-02 15:04:05"))
	// 教練發令。通知所有運動員開始跑步, 即喚起之前 wait()的所有協程
	c.Broadcast()
	// 等待跑步
	time.Sleep(time.Second * 5)
}

執行結果如下:

如結果所示, 10個運動員在8秒內分別準備好,等待教練發令後,同時開跑。
此場景中,10個運動員相當於10個協程, 同時等待主協程的命令,使用Broadcast() 喚醒所有阻塞的子協程。

注意事項

使用 Cond,最容易踩的坑就是呼叫 Wait() 方法之前,呼叫者沒有持有鎖或沒有檢查輔助條件。
在如上範例程式碼中,假如把呼叫 Wait() 方法前後的加鎖和釋放鎖的程式碼註釋掉,執行程式碼會導致程式 panic。原因是呼叫 Wait 方法,會先把呼叫者放入等待佇列中,然後釋放鎖。此時如果在未持有鎖時呼叫釋放鎖的方法,就會導致程式 panic。

到此這篇關於Go中sync 包的 Cond 使用的文章就介紹到這了,更多相關go sync包cond使用內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com