golang 條件變量詳解


1:為什么先要鎖定條件變量基於的互斥鎖,才能調用它的Wait方法?

2:為什么要用for語句來包裹調用其Wait方法的表達式,用if語句不行嗎?

這些問題我在面試的時候也經常問。你需要對這個Wait方法的內部機制有所了解才能回答上來。

條件變量的Wait方法主要做了四件事。

  1. 把調用它的 goroutine(也就是當前的 goroutine)加入到當前條件變量的通知隊列中。
  2. 解鎖當前的條件變量基於的那個互斥鎖。
  3. 讓當前的 goroutine 處於等待狀態,等到通知到來時再決定是否喚醒它。此時,這個 goroutine 就會阻塞在調用這個Wait方法的那行代碼上。
  4. 如果通知到來並且決定喚醒這個 goroutine,那么就在喚醒它之后重新鎖定當前條件變量基於的互斥鎖。自此之后,當前的 goroutine 就會繼續執行后面的代碼了。

問題一解答:因為條件變量的Wait方法在阻塞當前的 goroutine 之前,會解鎖它基於的互斥鎖,所以在調用該Wait方法之前,我們必須先鎖定那個互斥鎖,否則在調用這個Wait方法時,就會引發一個不可恢復的 panic。為什么條件變量的Wait方法要這么做呢?你可以想象一下,如果Wait方法在互斥鎖已經鎖定的情況下,阻塞了當前的 goroutine,那么又由誰來解鎖呢?別的 goroutine 嗎?先不說這違背了互斥鎖的重要使用原則,即:成對的鎖定和解鎖,就算別的 goroutine 可以來解鎖,那萬一解鎖重復了怎么辦?由此引發的 panic 可是無法恢復的。如果當前的 goroutine 無法解鎖,別的 goroutine 也都不來解鎖,那么又由誰來進入臨界區,並改變共享資源的狀態呢?只要共享資源的狀態不變,即使當前的 goroutine 因收到通知而被喚醒,也依然會再次執行這個Wait方法,並再次被阻塞。所以說,如果條件變量的Wait方法不先解鎖互斥鎖的話,那么就只會造成兩種后果:不是當前的程序因 panic 而崩潰,就是相關的 goroutine 全面阻塞。

問題二解答:這主要是為了保險起見。如果一個 goroutine 因收到通知而被喚醒,但卻發現共享資源的狀態,依然不符合它的要求,那么就應該再次調用條件變量的Wait方法,並繼續等待下次通知的到來。

  1. 有多個 goroutine 在等待共享資源的同一種狀態。比如,它們都在等mailbox變量的值不為0的時候再把它的值變為0,這就相當於有多個人在等着我向信箱里放置情報。雖然等待的 goroutine 有多個,但每次成功的 goroutine 卻只可能有一個。別忘了,條件變量的Wait方法會在當前的 goroutine 醒來后先重新鎖定那個互斥鎖。在成功的 goroutine 最終解鎖互斥鎖之后,其他的 goroutine 會先后進入臨界區,但它們會發現共享資源的狀態依然不是它們想要的。這個時候,for循環就很有必要了。
  2. 共享資源可能有的狀態不是兩個,而是更多。比如,mailbox變量的可能值不只有0和1,還有2、3、4。這種情況下,由於狀態在每次改變后的結果只可能有一個,所以,在設計合理的前提下,單一的結果一定不可能滿足所有 goroutine 的條件。那些未被滿足的 goroutine 顯然還需要繼續等待和檢查。
  3. 有一種可能,共享資源的狀態只有兩個,並且每種狀態都只有一個 goroutine 在關注,就像我們在主問題當中實現的那個例子那樣。不過,即使是這樣,使用for語句仍然是有必要的。原因是,在一些多 CPU 核心的計算機系統中,即使沒有收到條件變量的通知,調用其Wait方法的 goroutine 也是有可能被喚醒的。這是由計算機硬件層面決定的,即使是操作系統(比如 Linux)本身提供的條件變量也會如此

來看下signal()代碼示列

type User struct { //模擬用戶
    mail *Mail
}

type Mail struct { //模擬郵箱
    mail bool
    sendCond *sync.Cond
    recvCond *sync.Cond
    lock sync.RWMutex
    sendName string
    recvName string
}

func  NewMail() *Mail {
    m := Mail{}
    m.sendCond = sync.NewCond(&m.lock)
    m.recvCond = sync.NewCond(m.lock.RLocker())
    return &m
}

func (m *Mail) SendMail() {
    m.lock.Lock()
    time.Sleep(time.Minute)
    if m.mail{
        m.sendCond.Wait()
        fmt.Println("已經有郵件了等待接收")
    }
    m.mail = true
    fmt.Println("發送了一封郵件給:",m.recvName)
    m.lock.Unlock()
    m.recvCond.Signal() //向接收者發送消息
}

func (m *Mail) RecvMail() {
    m.lock.RLock()
    if !m.mail{
        fmt.Println("沒有郵件,等待郵箱發送郵件")
        m.recvCond.Wait()

    }
    m.mail  = false
    fmt.Printf("%s收到一封%s發送的郵件\n",m.recvName,m.sendName)
    m.lock.RUnlock()
    m.sendCond.Signal()
}


func main()  {
    mail := NewMail()
    lock := make(chan struct{},2)
    use1 := User{}
    use1.mail = mail
    use1.mail.sendName = "jacky"
    use2 := User{}
    use2.mail = mail
    use2.mail.recvName = "rose"

    //send mail
    go func() {
        defer func() {
            lock <- struct{}{}
        }()

        for{
            use1.mail.SendMail()
            time.Sleep(time.Second)
        }
    }()

    go func() {
        defer func() {
            lock <- struct{}{}
        }()
        for{
            use2.mail.RecvMail()
            time.Sleep(time.Second)
        }
    }()

    <-lock
    <-lock
}

我們再看下條件變量的Broadcast()通知方法示列

func main() {
    // mailbox 代表信箱。
    // 0代表信箱是空的,1代表信箱是滿的。
    var mailbox uint8
    // lock 代表信箱上的鎖。
    var lock sync.Mutex
    // sendCond 代表專用於發信的條件變量。
    sendCond := sync.NewCond(&lock)
    // recvCond 代表專用於收信的條件變量。
    recvCond := sync.NewCond(&lock)

    // send 代表用於發信的函數。
    send := func(id, index int) {
        lock.Lock()
        for mailbox == 1 {
            sendCond.Wait()
        }
        log.Printf("發送郵件用戶%d:准備發送郵件...",index)
        mailbox = 1
        log.Printf("發送郵件用戶%d:發送了一份郵件...",index)
        lock.Unlock()
        recvCond.Broadcast()
    }

    // recv 代表用於收信的函數。
    recv := func(id, index int) {
        lock.Lock()
        for mailbox == 0 {
            recvCond.Wait()
        }
        log.Printf("用戶%d收到郵件:發現有份郵件待接收...",index)
        mailbox = 0
        log.Printf("用戶%d收到郵件:收到一份郵件...",index)
        lock.Unlock()
        sendCond.Signal() // 確定只會有一個發信的goroutine。
    }

    // sign 用於傳遞演示完成的信號。
    sign := make(chan struct{}, 3)
    max := 6
    go func(id, max int) { // 用於發信。
        defer func() {
            sign <- struct{}{}
        }()
        for i := 1; i <= max; i++ {
            time.Sleep(time.Millisecond * 500)
            send(id, i)
        }
    }(0, max)
    go func(id, max int) { // 用於收信。
        defer func() {
            sign <- struct{}{}
        }()
        for j := 1; j <= max; j++ {
            time.Sleep(time.Millisecond * 200)
            recv(id, j)
        }
    }(1, max/2)
    go func(id, max int) { // 用於收信。
        defer func() {
            sign <- struct{}{}
        }()
        for k := 1; k <= max; k++ {
            time.Sleep(time.Millisecond * 200)
            recv(id, k)
        }
    }(2, max/2)

    <-sign
    <-sign
    <-sign
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM