欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文帶你深入理解Go語言中的sync.Cond

 更新時間:2023年01月31日 15:23:25   作者:eleven26  
sync.Cond?表示的是條件變量,它是一種同步機制,用來協(xié)調多個?goroutine?之間的同步。本文將通過示例為大家介紹Go語言中sync.Cond的使用,需要的可以參考一下

在 go 的標準庫中,提供了 sync.Cond 這個并發(fā)原語,讓我們可以實現(xiàn)多個 goroutine 等待某一條件滿足之后再繼續(xù)執(zhí)行。 它需要配合 sync.Mutex 一起使用,因為 CondWait 方法需要在 Mutex 的保護下才能正常工作。 對于條件變量,可能大多數(shù)人只是知道它的存在,但是用到它的估計寥寥無幾,因為很多并發(fā)場景的處理都能使用 chan 來實現(xiàn), 而且 chan 的使用也更加簡單。 但是在某些場景下,Cond 可能是最好的選擇,本文就來探討一下 Cond 的使用場景,基本用法,以及它的實現(xiàn)原理。

sync.Cond 是什么

sync.Cond 表示的是條件變量,它是一種同步機制,用來協(xié)調多個 goroutine 之間的同步,當共享資源的狀態(tài)發(fā)生變化的時候, 可以通過條件變量來通知所有等待的 goroutine 去重新獲取共享資源。

適用場景

在實際使用中,我們可能會有多個 goroutine 在執(zhí)行的過程中,由于某一條件不滿足而阻塞的情況。 這個時候,我們就可以使用條件變量來實現(xiàn) goroutine 之間的同步。比如,我們有一個 goroutine 用來獲取數(shù)據(jù), 但是可能會比較耗時,這個時候,我們就可以使用條件變量來實現(xiàn) goroutine 之間的同步, 當數(shù)據(jù)準備好之后,就可以通過條件變量來通知所有等待的 goroutine 去重新獲取共享資源。

sync.Cond 條件變量用來協(xié)調想要訪問共享資源的那些 goroutine,當共享資源的狀態(tài)發(fā)生變化的時候, 它可以用來通知所有等待的 goroutine 去重新獲取共享資源。

sync.Cond 的基本用法

sync.Cond 的基本用法非常簡單,我們只需要通過 sync.NewCond 方法來創(chuàng)建一個 Cond 實例, 然后通過 Wait 方法來等待條件滿足,通過 Signal 或者 Broadcast 方法來通知所有等待的 goroutine 去重新獲取共享資源。

NewCond 創(chuàng)建實例

sync.NewCond 方法用來創(chuàng)建一個 Cond 實例,它的參數(shù)是一個 Locker 接口,我們可以傳入一個 Mutex 或者 RWMutex 實例。 這個條件變量的 Locker 接口就是用來保護共享資源的。

Wait 等待條件滿足

Wait 方法用來等待條件滿足,它會先釋放 Cond 的鎖(Cond.L),然后阻塞當前 goroutine(實際調用的是 goparkunlock),直到被 Signal 或者 Broadcast 喚醒。

它做了如下幾件事情:

  • 釋放 Cond 的鎖(Cond.L),然后阻塞當前 goroutine。(所以,使用之前需要先鎖定)
  • Signal 或者 Broadcast 喚醒之后,會重新獲取 Cond 的鎖(Cond.L)。
  • 之后,就返回到 goroutine 阻塞的地方繼續(xù)執(zhí)行。

Signal 通知一個等待的 goroutine

Signal 方法用來通知一個等待的 goroutine,它會喚醒一個等待的 goroutine,然后繼續(xù)執(zhí)行當前 goroutine。 如果沒有等待的 goroutine,則不會有任何操作。

Broadcast 通知所有等待的 goroutine

Broadcast 方法用來通知所有等待的 goroutine,它會喚醒所有等待的 goroutine,然后繼續(xù)執(zhí)行當前 goroutine。 如果沒有等待的 goroutine,則不會有任何操作。

sync.Cond 使用實例

下面我們通過一個實例來看一下 sync.Cond 的使用方法。

package cond

import (
   "fmt"
   "sync"
   "testing"
   "time"
)

var done bool
var data string

func write(c *sync.Cond) {
   fmt.Println("writing.")
   // 讓 reader 先獲取鎖,模擬條件不滿足然后 wait 的情況
   time.Sleep(time.Millisecond * 10)
   c.L.Lock()
   // 模擬耗時的寫操作
   time.Sleep(time.Millisecond * 50)
   data = "hello world"
   done = true
   fmt.Println("writing done.")
   c.L.Unlock()
   c.Broadcast()
}

func read(c *sync.Cond) {
   fmt.Println("reading")
   c.L.Lock()
   for !done {
      fmt.Println("reader wait.")
      c.Wait()
   }
   fmt.Println("read done.")
   fmt.Println("data:", data)
   defer c.L.Unlock()
}

func TestCond(t *testing.T) {
   var c = sync.NewCond(&sync.Mutex{})

   go read(c)  // 讀操作
   go read(c)  // 讀操作
   go write(c) // 寫操作

   time.Sleep(time.Millisecond * 100) // 等待操作完成
}

輸出:

reading
reader wait. // 還沒獲取完數(shù)據(jù),需要等待
writing.
reading
reader wait.
writing done. // 獲取完數(shù)據(jù)了,通知所有等待的 reader
read done. // 讀取到數(shù)據(jù)了
data: hello world // 輸出讀取到的數(shù)據(jù)
read done.
data: hello world

這個例子可以粗略地用下圖來表示:

說明:

  • read1reader2 表示兩個 goroutine,它們都會調用 read 函數(shù)。
  • donefalse 的時候,reader1reader2 都會調用 c.Wait() 函數(shù),然后阻塞等待。
  • write 表示一個 goroutine,它會調用 write 函數(shù)。
  • write 函數(shù)中,獲取完數(shù)據(jù)之后,會將 done 設置為 true,然后調用 c.Broadcast() 函數(shù),通知所有等待的 reader 去重新獲取共享資源。
  • reader1reader2 在解除阻塞狀態(tài)后,都會重新獲取共享資源,然后輸出讀取到的數(shù)據(jù)。

在這個例子中,done 的功能是標記,用來表示共享資源是否已經(jīng)獲取完畢,如果沒有獲取完畢,那么 reader 就會阻塞等待。

為什么要用 sync.Cond

在文章開頭,我們說了,很多并發(fā)編程的問題都可以通過 channel 來解決。 同樣的,在上面提到的 sync.Cond 的使用場景,使用 channel 也是可以實現(xiàn)的, 我們只要 close(ch) 來關閉 channel 就可以實現(xiàn)通知多個等待的協(xié)程了。

那么為什么還要用 sync.Cond 呢? 主要原因是,sync.Cond 可以重復地進行 Wait()Signal()Broadcast() 操作, 但是,如果想通過關閉 chan 來實現(xiàn)這個功能的話,那就只能通知一次了。 因為 channel 只能關閉一次,關閉一個已經(jīng)關閉的 channel 會導致程序 panic。

使用 channel 的另外一種方式是,記錄 reader 的數(shù)量,然后通過往 channel 中發(fā)送多次數(shù)據(jù)來實現(xiàn)通知多個 reader。 但是這樣一來代碼就會復雜很多,從另一個角度說,出錯的概率大了很多。

close channel 廣播實例

下面的例子模擬了使用 close(chan) 來實現(xiàn) sync.Cond 中那種廣播功能,但是只能通知一次。

package close_chan

import (
   "fmt"
   "testing"
   "time"
)

var data string

func read(c <-chan struct{}) {
   fmt.Println("reading.")

   // 從 chan 接收數(shù)據(jù),如果 chan 中沒有數(shù)據(jù),會阻塞。
   // 如果能接收到數(shù)據(jù),或者 chan 被關閉,會解除阻塞狀態(tài)。
   <-c

   fmt.Println("data:", data)
}

func write(c chan struct{}) {
   fmt.Println("writing.")
   // 模擬耗時的寫操作
   time.Sleep(time.Millisecond * 10)
   data = "hello world"
   fmt.Println("write done.")

   // 關閉 chan 的時候,會通知所有的 reader
   // 所有等待從 chan 接收數(shù)據(jù)的 goroutine 都會被喚醒
   close(c)
}

func TestCloseChan(t *testing.T) {
   ch := make(chan struct{})

   go read(ch)
   go read(ch)
   go write(ch)

   // 不能關閉已經(jīng)關閉的 chan
   time.Sleep(time.Millisecond * 20)
   // panic: close of closed channel
   // 下面這行代碼會導致 panic
   //go write(ch)

   time.Sleep(time.Millisecond * 100)
}

輸出:

writing.
reading. // 會阻塞直到寫完
reading. // 會阻塞直到寫完
write done. // 寫完之后,才能讀
data: hello world
data: hello world

上面例子的 write 不能多次調用,否則會導致 panic。

sync.Cond 基本原理

go 的 sync.Cond 中維護了一個鏈表,這個鏈表記錄了所有阻塞的 goroutine,也就是由于調用了 Wait 而阻塞的 goroutine。 而 SignalBroadcast 方法就是用來喚醒這個鏈表中的 goroutine 的。 Signal 方法只會喚醒鏈表中的第一個 goroutine,而 Broadcast 方法會喚醒鏈表中的所有 goroutine。

下圖是 Signal 方法的效果,可以看到,Signal 方法只會喚醒鏈表中的第一個 goroutine

說明:

  • notifyListsync.Cond 中維護的一個鏈表,這個鏈表記錄了所有阻塞的 goroutine。
  • head 是鏈表的頭節(jié)點,tail 是鏈表的尾節(jié)點。
  • Signal 方法只會喚醒鏈表中的第一個 goroutine

Broadcast 方法會喚醒 notifyList 中的所有 goroutine。

sync.Cond 的設計與實現(xiàn)

最后,我們來看一下 sync.Cond 的設計與實現(xiàn)。

sync.Cond 模型

sync.Cond 的模型如下所示:

type Cond struct {
   noCopy noCopy

   // L is held while observing or changing the condition
   L Locker // L 在觀察或改變條件時被持有

   notify  notifyList
   checker copyChecker
}

屬性說明:

  • noCopy 是一個空結構體,用來檢查 sync.Cond 是否被復制。(在編譯前通過 go vet 命令來檢查)
  • L 是一個 Locker 接口,用來保護條件變量。
  • notify 是一個 notifyList 類型,用來記錄所有阻塞的 goroutine。
  • checker 是一個 copyChecker 類型,用來檢查 sync.Cond 是否被復制。(如果在運行時被復制,會導致 panic

notifyList 結構體

notifyListsync.Cond 中維護的一個鏈表,這個鏈表記錄了所有因為共享資源還沒準備好而阻塞的 goroutine。它的定義如下所示:

type notifyList struct {
   wait atomic.Uint32
   notify uint32

   // 阻塞的 waiter 名單。
   lock mutex // 鎖
   head *sudog // 阻塞的 goroutine 鏈表(鏈表頭)
   tail *sudog // 阻塞的 goroutine 鏈表(鏈表尾)
}

屬性說明:

  • wait 是下一個 waiter 的編號。它在鎖外自動遞增。
  • notify 是下一個要通知的 waiter 的編號。它可以在鎖外讀取,但只能在持有鎖的情況下寫入。
  • lock 是一個 mutex 類型,用來保護 notifyList
  • head 是一個 sudog 類型,用來記錄阻塞的 goroutine 鏈表的頭節(jié)點。
  • tail 是一個 sudog 類型,用來記錄阻塞的 goroutine 鏈表的尾節(jié)點。

notifyList 的方法說明:

notifyList 中包含了幾個操作阻塞的 goroutine 鏈表的方法。

  • notifyListAdd 方法將 waiter 的編號加 1。
  • notifyListWait 方法將當前的 goroutine 加入到 notifyList 中。(也就是將當前協(xié)程掛起)
  • notifyListNotifyOne 方法將 notifyList 中的第一個 goroutine 喚醒。
  • notifyListNotifyAll 方法將 notifyList 中的所有 goroutine 喚醒。
  • notifyListCheck 方法檢查 notifyList 的大小是否正確。

sync.Cond 的方法

notifyList 就不細說了,本文重點講解一下 sync.Cond 的實現(xiàn)。

Wait 方法

Wait 方法用在當條件不滿足的時候,將當前運行的協(xié)程掛起。

func (c *Cond) Wait() {
   // 檢查是否被復制
   c.checker.check()
   // 更新 notifyList 中需要等待的 waiter 的數(shù)量
   // 返回當前需要插入 notifyList 的編號
   t := runtime_notifyListAdd(&c.notify)
   // 解鎖
   c.L.Unlock()
   // 掛起當前 g,直到被喚醒
   runtime_notifyListWait(&c.notify, t)
   // 喚醒之后,重新加鎖。
   // 因為阻塞之前解鎖了。
   c.L.Lock()
}

對于 Wait 方法,我們需要注意的是,使用之前,我們需要先調用 L.Lock() 方法加鎖,然后再調用 Wait 方法,否則會報錯。

文檔里面的例子:

c.L.Lock()
for !condition() {
    c.Wait()
}
// ...使用條件...
// 這里是我們在條件滿足之后,需要執(zhí)行的代碼。
c.L.Unlock()

好了,問題來了,調用 Wait 方法之前為什么要先加鎖呢?

這是因為在我們使用共享資源的時候,可能一些代碼是互斥的,所以我們需要加鎖。 這樣我們就可以保證在我們使用共享資源的時候,不會被其他協(xié)程修改。 但是如果因為條件不滿足,我們需要等待的話,我們不可能在持有鎖的情況下等待, 因為在修改條件的時候,可能也需要加鎖,這樣就會造成死鎖。

另外一個問題是,為什么要使用 for 來檢查條件是否滿足,而不是使用 if 呢?

這是因為在我們調用 Wait 方法之后,可能會有其他協(xié)程喚醒我們,但是條件并沒有滿足, 這個時候依然是需要繼續(xù) Wait 的。

Signal 方法

Signal 方法用在當條件滿足的時候,將 notifyList 中的第一個 goroutine 喚醒。

func (c *Cond) Signal() {
   // 檢查 sync.Cond 是否被復制了
   c.checker.check()
   // 喚醒 notifyList 中的第一個 goroutine
   runtime_notifyListNotifyOne(&c.notify)
}

Broadcast 方法

Broadcast 方法用在當條件滿足的時候,將 notifyList 中的所有 goroutine 喚醒。

func (c *Cond) Broadcast() {
   // 檢查 sync.Cond 是否被復制了
   c.checker.check()
   // 喚醒 notifyList 中的所有 goroutine
   runtime_notifyListNotifyAll(&c.notify)
}

copyChecker 結構體

copyChecker 結構體用來檢查 sync.Cond 是否被復制。它實際上只是一個 uintptr 類型的值。

type copyChecker uintptr

// check 方法檢查 copyChecker 是否被復制了。
func (c *copyChecker) check() {
   if uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
      !atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
      uintptr(*c) != uintptr(unsafe.Pointer(c)) {
      panic("sync.Cond is copied")
   }
}

copyChecker 的值只有兩種可能:

  • 0,表示還沒有調用過 Wait, SignalBroadcast 方法。
  • uintptr(unsafe.Pointer(&copyChecker)),表示已經(jīng)調用過 Wait, SignalBroadcast 方法。在這幾個方法里面會調用 check 方法,所以 copyChecker 的值會被修改。

所以如果 copyChecker 的值不是 0,也不是 uintptr(unsafe.Pointer(&copyChecker))(也就是最初的 copyChecker 的內(nèi)存地址),則表示 copyChecker 被復制了。

需要注意的是,這個方法在調用 CompareAndSwapUintptr 還會檢查一下,這是因為有可能會并發(fā)調用 CompareAndSwapUintptr, 如果另外一個協(xié)程調用了 CompareAndSwapUintptr 并且成功了,那么當前協(xié)程的這個 CompareAndSwapUintptr 調用會返回 false, 這個時候就需要檢查是否是因為另外一個協(xié)程調用了 CompareAndSwapUintptr 而導致的,如果是的話,就不會 panic

為什么 sync.Cond 不能被復制

從上一小節(jié)中我們可以看到,sync.Cond 其實是不允許被復制的,但是如果是在調用 Wait, SignalBroadcast 方法之前復制,那倒是沒關系。

這是因為 sync.Cond 中維護了一個阻塞的 goroutine 列表。如果 sync.Cond 被復制了,那么這個列表就會被復制,這樣就會導致兩個 sync.Cond 都包含了這個列表;但是我們喚醒的時候,只會有其中一個 sync.Cond 被喚醒,另外一個 sync.Cond 就會一直阻塞。 所以 go 直接從語言層面限制了這種情況,不允許 sync.Cond 被復制。

總結

sync.Cond 是一個條件變量,它可以用來協(xié)調多個 goroutine 之間的同步,當條件滿足的時候,去通知那些因為條件不滿足被阻塞的 goroutine 繼續(xù)執(zhí)行。

sync.Cond 的接口比較簡單,只有 Wait, SignalBroadcast 三個方法。

  • Wait 方法用來阻塞當前 goroutine,直到條件滿足。調用 Wait 方法之前,需要先調用 L.Lock 方法加鎖。
  • Signal 方法用來喚醒 notifyList 中的第一個 goroutine
  • Broadcast 方法用來喚醒 notifyList 中的所有 goroutine。

sync.Cond 的實現(xiàn)也比較簡單,它的核心就是 notifyList,它是一個鏈表,用來保存所有因為條件不滿足而被阻塞的 goroutine。

用關閉 channel 的方式也可以實現(xiàn)類似的廣播功能,但是有個問題是 channel 不能被重復關閉,所以這種方式無法被多次使用。也就是說使用這種方式無法多次廣播。

使用 channel 發(fā)送通知的方式也是可以的,但是這樣實現(xiàn)起來就復雜很多了,就更容易出錯了。

sync.Cond 中使用 copyChecker 來檢查 sync.Cond 是否被復制,如果被復制了,就會 panic。需要注意的是,這里的復制是指調用了 Wait,SignalBroadcast 方法之后,sync.Cond 被復制了。在調用這幾個方法之前進行復制是沒有影響的。

以上就是一文帶你深入理解Go語言中的sync.Cond的詳細內(nèi)容,更多關于Go語言 sync.Cond的資料請關注腳本之家其它相關文章!

相關文章

  • Go實現(xiàn)HTTP請求轉發(fā)的示例代碼

    Go實現(xiàn)HTTP請求轉發(fā)的示例代碼

    請求轉發(fā)是一項核心且常見的功能,本文主要介紹了Go實現(xiàn)HTTP請求轉發(fā)的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2025-05-05
  • golang文件服務器的兩種方式(可以訪問任何目錄)

    golang文件服務器的兩種方式(可以訪問任何目錄)

    這篇文章主要介紹了golang文件服務器的兩種方式,可以訪問任何目錄,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-04-04
  • Golang線上內(nèi)存爆掉問題排查(pprof)與解決

    Golang線上內(nèi)存爆掉問題排查(pprof)與解決

    這篇文章主要介紹了Golang線上內(nèi)存爆掉問題排查(pprof)與解決,涉及到數(shù)據(jù)敏感,文中代碼是我模擬線上故障的一個情況,好在我們程序都有添加pprof監(jiān)控,于是直接通過go tool pprof分析,需要的朋友可以參考下
    2024-04-04
  • golang移除數(shù)組中重復的元素操作

    golang移除數(shù)組中重復的元素操作

    這篇文章主要介紹了golang移除數(shù)組中重復的元素操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • 詳解Go并發(fā)編程時如何避免發(fā)生競態(tài)條件和數(shù)據(jù)競爭

    詳解Go并發(fā)編程時如何避免發(fā)生競態(tài)條件和數(shù)據(jù)競爭

    大家都知道,Go是一種支持并發(fā)編程的編程語言,但并發(fā)編程也是比較復雜和容易出錯的。比如本篇分享的問題:競態(tài)條件和數(shù)據(jù)競爭的問題
    2023-04-04
  • Go泛型之泛型約束示例詳解

    Go泛型之泛型約束示例詳解

    這篇文章主要給大家介紹了關于Go泛型之泛型約束的相關資料,泛型是靜態(tài)語言中的一種編程方式,這種編程方式可以讓算法不再依賴于某個具體的數(shù)據(jù)類型,而是通過將數(shù)據(jù)類型進行參數(shù)化,以達到算法可復用的目的,需要的朋友可以參考下
    2023-12-12
  • 詳解Golang中Channel的用法

    詳解Golang中Channel的用法

    如果說goroutine是Go語言程序的并發(fā)體的話,那么channels則是它們之間的通信機制。這篇文章主要介紹Golang中Channel的用法,需要的朋友可以參考下
    2020-11-11
  • 秒懂Golang匿名函數(shù)

    秒懂Golang匿名函數(shù)

    所謂匿名函數(shù),就是沒有名字的函數(shù),本文重點給大家介紹Golang匿名函數(shù)的相關知識,通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-02-02
  • 通過Golang實現(xiàn)無頭瀏覽器截圖

    通過Golang實現(xiàn)無頭瀏覽器截圖

    在Web開發(fā)中,有時需要對網(wǎng)頁進行截圖,以便進行頁面預覽、測試等操作,本文為大家整理了Golang實現(xiàn)無頭瀏覽器的截圖的方法,感興趣的可以了解一下
    2023-05-05
  • 一文帶你搞懂Golang依賴注入的設計與實現(xiàn)

    一文帶你搞懂Golang依賴注入的設計與實現(xiàn)

    在現(xiàn)代的 web 框架里面,基本都有實現(xiàn)了依賴注入的功能,可以讓我們很方便地對應用的依賴進行管理。今天我們來看看 go 里面實現(xiàn)依賴注入的一種方式,感興趣的可以了解一下
    2023-01-01

最新評論