一招教你無阻塞讀寫Golang channel
無論是無緩衝通道,還是有緩衝通道,都存在阻塞的情況,教你一招再也不遇到channel阻塞的問題。
這篇文章會介紹,哪些情況會存在阻塞,以及如何使用select解決阻塞。
阻塞場景
阻塞場景共4個,有快取和無緩衝各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 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實現無阻塞讀寫
ofollow,noindex" target="_blank">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
如果這篇文章對你有幫助,請點個贊/喜歡,讓我知道我的寫作是有價值的,感謝。