一招教你無阻塞讀寫Golang channel

無論是無緩衝通道,還是有緩衝通道,都存在阻塞的情況,教你一招再也不遇到channel阻塞的問題。

這篇文章會介紹,哪些情況會存在阻塞,以及如何使用select解決阻塞。

阻塞場景

阻塞場景共4個,有緩存和無緩衝各2個。

無緩衝通道的特點是,發送的數據需要被讀取後,發送纔會完成,它阻塞場景:

  1. 通道中無數據,但執行讀通道。
  2. 通道中無數據,向通道寫數據,但無協程讀取。
// 場景1
func ReadNoDataFromNoBufCh() {
    noBufCh := make(chan int)

    <-noBufCh
    fmt.Println("read from no buffer channel success")

    // Output:
    // fatal error: all goroutines are asleep - deadlock!
}

// 場景2
func WriteNoBufCh() {
    ch := make(chan int)

    ch <- 1
    fmt.Println("write success no block")
    
    // Output:
    // fatal error: all goroutines are asleep - deadlock!
}

注:示例代碼中的Output註釋代表函數的執行結果,每一個函數都由於阻塞在通道操作而無法繼續向下執行,最後報了死鎖錯誤。

有緩存通道的特點是,有緩存時可以向通道中寫入數據後直接返回,緩存中有數據時可以從通道中讀到數據直接返回,這時有緩存通道是不會阻塞的,它阻塞的場景是:

  1. 通道的緩存無數據,但執行讀通道。
  2. 通道的緩存已經佔滿,向通道寫數據,但無協程讀。
// 場景1
func ReadNoDataFromBufCh() {
    bufCh := make(chan int, 1)

    <-bufCh
    fmt.Println("read from no buffer channel success")

    // Output:
    // fatal error: all goroutines are asleep - deadlock!
}

// 場景2
func WriteBufChButFull() {
    ch := make(chan int, 1)
    // make ch full
    ch <- 100

    ch <- 1
    fmt.Println("write success no block")
    
    // Output:
    // fatal error: all goroutines are asleep - deadlock!
}

使用Select實現無阻塞讀寫

select是執行選擇操作的一個結構,它裏面有一組case語句,它會執行其中無阻塞的那一個,如果都阻塞了,那就等待其中一個不阻塞,進而繼續執行,它有一個default語句,該語句是永遠不會阻塞的,我們可以藉助它實現無阻塞的操作。

下面示例代碼是使用select修改後的無緩衝通道和有緩衝通道的讀寫,以下函數可以直接通過main函數調用,其中的Ouput的註釋是運行結果,從結果能看出,在通道不可讀或者不可寫的時候,不再阻塞等待,而是直接返回。

// 無緩衝通道讀
func ReadNoDataFromNoBufChWithSelect() {
    bufCh := make(chan int)

    if v, err := ReadWithSelect(bufCh); err != nil {
        fmt.Println(err)
    } else {
        fmt.Printf("read: %d\n", v)
    }

    // Output:
    // channel has no data
}

// 有緩衝通道讀
func ReadNoDataFromBufChWithSelect() {
    bufCh := make(chan int, 1)

    if v, err := ReadWithSelect(bufCh); err != nil {
        fmt.Println(err)
    } else {
        fmt.Printf("read: %d\n", v)
    }

    // Output:
    // channel has no data
}

// select結構實現通道讀
func ReadWithSelect(ch chan int) (x int, err error) {
    select {
    case x = <-ch:
        return x, nil
    default:
        return 0, errors.New("channel has no data")
    }
}

// 無緩衝通道寫
func WriteNoBufChWithSelect() {
    ch := make(chan int)
    if err := WriteChWithSelect(ch); err != nil {
        fmt.Println(err)
    } else {
        fmt.Println("write success")
    }

    // Output:
    // channel blocked, can not write
}

// 有緩衝通道寫
func WriteBufChButFullWithSelect() {
    ch := make(chan int, 1)
    // make ch full
    ch <- 100
    if err := WriteChWithSelect(ch); err != nil {
        fmt.Println(err)
    } else {
        fmt.Println("write success")
    }

    // Output:
    // channel blocked, can not write
}

// select結構實現通道寫
func WriteChWithSelect(ch chan int) error {
    select {
    case ch <- 1:
        return nil
    default:
        return errors.New("channel blocked, can not write")
    }
}

使用Select+超時改善無阻塞讀寫

使用default實現的無阻塞通道阻塞有一個缺陷:當通道不可讀或寫的時候,會即可返回。實際場景,更多的需求是,我們希望,嘗試讀一會數據,或者嘗試寫一會數據,如果實在沒法讀寫,再返回,程序繼續做其它的事情。

使用定時器替代default可以解決這個問題。比如,我給通道讀寫數據的容忍時間是500ms,如果依然無法讀寫,就即刻返回,修改一下會是這樣:

func ReadWithSelect(ch chan int) (x int, err error) {
    timeout := time.NewTimer(time.Microsecond * 500)

    select {
    case x = <-ch:
        return x, nil
    case <-timeout.C:
        return 0, errors.New("read time out")
    }
}

func WriteChWithSelect(ch chan int) error {
    timeout := time.NewTimer(time.Microsecond * 500)

    select {
    case ch <- 1:
        return nil
    case <-timeout.C:
        return errors.New("write time out")
    }
}

結果就會變成超時返回:

read time out
write time out
read time out
write time out
如果這篇文章對你有幫助,請點個贊/喜歡,讓我知道我的寫作是有價值的,感謝。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章