Hello,大家好,又見面了!上一遍我們將 channel 相關基礎以及使用場景。這一篇,還需要再次進階理解channel 阻塞問題。以下創建一個chan類型為int,cap 為3。
ch := make(chan string,1)
channel 內部其實是一個環形buf數據結構,是一種滑動窗口機制,當make完后,就分配在 Heap 上。
Channel 是如何發送數據和接收數據,會有什么問題出現,面試也是常見。
設 G1 為發送者:
ch <- "hello"
上面,向 chan 發送一條“hello”數據:
- 第一步: acquire lock
- 第二步:enqueue(把“hello”拷貝buf數組里)
- 第三步:release lock
如果 G1 發送數據超過指定cap時,會出現什么情況?
看下面實例:
ch := make(chan int,1)
ch <- 100
ch <- 200
以上會出現什么,chan 緩沖區允許大小為1,如果再往chan仍數據,滿了就會被阻塞,那么是如何實現阻塞的呢?當 chan 滿時,會進入 gopark,此時 G1 進入一個 waiting 狀態,然后會創建一個 sudog 對象,其實就sendq隊列,把 200放進去。等 buf 不滿的時候,再喚醒放入buf里面。
通過如下源碼,你會更加清晰:
type hchan struct {
//省略部分代碼。。。
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
//省略部分代碼。。。
}
type waitq struct {
first *sudog
last *sudog
}
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer // data element (may point to stack)
acquiretime int64
releasetime int64
ticket uint32
isSelect bool
success bool
parent *sudog // semaRoot binary tree
waitlink *sudog // g.waiting list or semaRoot
waittail *sudog // semaRoot
c *hchan // channel
}
設 G2 為接收者:
d := <- ch
上面,從 chan 獲取數據:
- 第一步:也是先獲取鎖
- 第二步:從 buf 數據里面拷貝,賦給 d 變量
- 第三步:release lock
Go 語言核心思想:“Do not communicate by sharing memory; instead, share memory by communicating.” 你可以看看這本書名叫:Effective Go
如果接收者,接收一個空對象,也會發生什么情況?
代碼示例:
ch := make(chan int,1)
t := <- ch
也會報錯如下:
fatal error: all goroutines are asleep - deadlock!
上面,從 chan 取出數據,可是沒有數據了。此時,它會把 接收者 G2 阻塞掉,也是和G1發送者一樣,也會執行 gopark 將狀態改為 waiting,不一樣的點就是。
正常情況下,接收者G2作為取出數據是去 buf 讀取數據的,但現在,buf 為空了,此時,接收者G2會將sudog導出來,因為現在G2已經被阻塞了嘛,會把G2給G,然后將t := <-ch中變量 t 是在棧上的地址,放進去elem,也就是說,只存它的地址指針在sudog里面。
最后,ch <- 200 當G1往 chan 添加200這個數據,正常情況是將數據添加到buf里面,然后喚醒 G2 是吧,而現在是將 G1 的添加200數據直接干到剛才G2阻塞的t這里變量里面。
你會認為,這樣真的可以嗎?想一想,G2 本來就是已經阻塞了,然后我們直接這么干肯定沒有什么毛病,而且效率提高了,不需要再次放入buf再取出,這個過程也是需要時間。不然,不得往chan添加數據需要加鎖、拷貝、解鎖一序列操作,那肯定就慢了,我想Go語言是為了高效及內存使用率的考慮這樣設計的。(注意,一般都是在runtime里面完成,不然會出現象安全問題。)
總結:
chan 類型的特點:chan 如果為空,receiver 接收數據的時候就會阻塞等待,直到 chan 被關閉或者有新的數據到來。有這種個機制,就可以實現 wait/notify 的設計模式。
使用 channel 要思考的問題?
- 如果N個發送者發送到chan及N個接收者,這樣會頻繁導致鎖頻繁爭用;
- 如果N個往 chan 發送,而 buf很小,會導致 Goroutine 不斷被gopark,然后runtime開銷就大了;
相關面試題:
- channel 有緩沖區和無緩沖區別?
- channel 線程安全嗎?
- 是否了解 channel 底層實現,比如channel 底層數據結構?環形buf