深入解析Go Channel各狀態(tài)下的操作結(jié)果
大家好,我是漁夫子。
channel是golang中獨(dú)有的特性,也是面試中經(jīng)常被問(wèn)到的。相信大家都看到過(guò)下面這張圖,對(duì)于不同狀態(tài)下通道,在操作時(shí)會(huì)有什么結(jié)果。
這張圖總結(jié)的非常好。但我們不能死記硬背這些結(jié)果。要了解其底層的基本原理,就能理解這些結(jié)果是怎么來(lái)的。
我們分三部分來(lái)講。先是channel的基礎(chǔ)使用,基礎(chǔ)使用提現(xiàn)了channel有哪些特性。再引出channel的底層數(shù)據(jù)結(jié)構(gòu)。底層數(shù)據(jù)結(jié)構(gòu)就是圍繞這些特性而建立的。最后再看go是如何基于底層數(shù)據(jù)結(jié)構(gòu)來(lái)實(shí)現(xiàn)這些特性的。
channel的基礎(chǔ)使用
通道的定義和初始化
通過(guò)var定義通道
通過(guò)var定義一個(gè)通道變量ch,這個(gè)變量能夠接收整型的數(shù)據(jù)。當(dāng)然也可以指定其他任何數(shù)據(jù)類型。
var ch chan int
- ch 代表變量名
- chan固定值。代表ch是通道類型
- int代表在通道ch中存儲(chǔ)的是整型數(shù)據(jù)。
- ch變量的默認(rèn)值是nil。對(duì)于nil通道在操作時(shí)會(huì)有特殊的場(chǎng)景,一會(huì)我們也會(huì)講解。
通過(guò)make初始化通道
通過(guò)make可以初始化無(wú)緩沖區(qū)通道和緩沖區(qū)通道。區(qū)別就在于make中是否指定了緩沖區(qū)的大小。如下:
var ch = make(chan int) //初始化無(wú)緩沖通道
var ch = make(chan int, 10) //緩沖區(qū)通道,緩沖區(qū)可以存10個(gè)元素
無(wú)緩沖通道和有緩沖通道的區(qū)別可以從屬性上和行為兩方面來(lái)體現(xiàn):
- 從屬性上區(qū)別:通道是否有一段緩沖區(qū)來(lái)暫存元素。
- 從行為上區(qū)別:發(fā)送者和接收者是否同步的還是異步的。
- 從底層數(shù)據(jù)結(jié)構(gòu)上區(qū)別:是否有一塊緩沖區(qū)來(lái)暫存數(shù)據(jù)。這個(gè)后面會(huì)詳細(xì)講解。
通道的操作
golang中對(duì)于通道有三種操作:往通道中發(fā)送元素、從通道中接收元素、關(guān)閉通道。如下:往通道中發(fā)送元素:
var ch chan int = make(chan int, 10)
2 ->ch //發(fā)送元素
var item int
item <-ch //接收元素
close(ch) //關(guān)閉元素
總結(jié)一下:
- 通道有三種操作:發(fā)送、接收和關(guān)閉。
- 通道有三種類型:nil通道、無(wú)緩沖通道和有緩沖通道。
- 通道有2種狀態(tài):關(guān)閉狀態(tài)和未關(guān)閉狀態(tài)。
- 緩沖通道的未關(guān)閉狀態(tài)又可以分為緩沖區(qū)滿、緩沖區(qū)未滿狀態(tài)。
那么,通道是基于怎樣的數(shù)據(jù)結(jié)構(gòu)來(lái)完成這些行為的呢?
channel的數(shù)據(jù)結(jié)構(gòu)
我們先給出channel的底層數(shù)據(jù)結(jié)構(gòu),如下:
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
type waitq struct {
first *sudog
last *sudog
}
根據(jù)上面的結(jié)構(gòu)定義,依次解釋下各個(gè)字段的含義:
- buf:指向一個(gè)數(shù)組,代表的是一個(gè)隊(duì)列,結(jié)合sendx和recvx字段實(shí)現(xiàn)了環(huán)形隊(duì)列。緩存對(duì)應(yīng)的元素。緩沖區(qū)通道就是利用這個(gè)字段實(shí)現(xiàn)的。
- qcount:在buf隊(duì)列中當(dāng)前有多少個(gè)元素。
- dataqsiz:代表隊(duì)列buf的容量。在使用make進(jìn)行初始化時(shí),指定的元素個(gè)數(shù)就存在該字段中。
- elemsize:一個(gè)元素的字節(jié)大小。根據(jù)該元素的大小,可以初始化buf的容量的大小。通過(guò)elemsize*容量就能知道該給buf分配多少字節(jié)的空間了。
- closed:代表該通道是否被關(guān)閉。其值只有0和1。1代表該通道已經(jīng)關(guān)閉了。0代表未關(guān)閉。
- elemtype:代表元素的類型。
- sendx:代表的是發(fā)送下一個(gè)元素應(yīng)該存儲(chǔ)的位置
- recvx:代表的是下一個(gè)接收元素的位置。
- recvq:代表的是等待接收元素的協(xié)程隊(duì)列
- sendq:代表的是發(fā)送元素的協(xié)程隊(duì)列。
根據(jù)以上結(jié)果,繪制成圖會(huì)容易理解點(diǎn),如下:
緩沖通道和非緩沖通道的區(qū)別
從定義上,緩沖通道和非緩沖通道都是通過(guò)make來(lái)初始化的。不同點(diǎn)在于是否在make函數(shù)上指定了通道的容量大小。如下:
unbufferCh := make(chan int) //初始化非緩沖區(qū)通道
bufferCh := make(chan int, 10) //初始化一個(gè)能緩沖10個(gè)元素的通道
從通道的底層數(shù)據(jù)結(jié)構(gòu)上來(lái)說(shuō),非緩沖渠道不會(huì)初始化結(jié)構(gòu)體中的buf字段。而緩沖渠道則會(huì)初始化buf字段。該字段指向一塊內(nèi)存區(qū)域。如下圖:
通道的發(fā)送、接收流程
通過(guò)源碼我們梳理出來(lái)了給通道發(fā)送數(shù)據(jù)和從通道中接收數(shù)據(jù)的流程圖。這張流程圖將緩沖通道和無(wú)緩沖通道兩種狀態(tài)下的發(fā)送和接收流程都包含了,所以看起來(lái)會(huì)比較復(fù)雜。但是沒(méi)關(guān)系,下面我們會(huì)分解這張圖。
通過(guò)上面的流程,大家需要注意的一點(diǎn)就是,無(wú)論是在發(fā)送還是接收操作時(shí),都是優(yōu)先從等待隊(duì)列中獲取對(duì)應(yīng)的線程,如果有,則直接接收或發(fā)送;如果等待隊(duì)列沒(méi)有協(xié)程,然后再看是否有緩沖區(qū)。這一點(diǎn)需要大家額外注意。
各狀態(tài)通道的操作
無(wú)緩沖通道
根據(jù)上述無(wú)緩沖通道其實(shí)本質(zhì)上就是沒(méi)有緩沖區(qū)。在初始化時(shí)不指定make的容量即可。實(shí)際上這也叫做同步發(fā)送和接收。針對(duì)這種狀態(tài)的通道,當(dāng)發(fā)送數(shù)據(jù)時(shí),如果接收隊(duì)列中有等待的接收協(xié)程,那么就能發(fā)送成功;否則,進(jìn)入阻塞狀態(tài)。反之,亦然。其流程圖就是圖中的紅色箭頭部分,如下:
再簡(jiǎn)化一下就是:
- 往無(wú)緩沖區(qū)中發(fā)送數(shù)據(jù)時(shí),如果有等待接收的協(xié)程,則發(fā)送成功;否則,發(fā)送協(xié)程進(jìn)入阻塞狀態(tài)。
- 從無(wú)緩沖區(qū)接收數(shù)據(jù)時(shí),如果有等待發(fā)送的協(xié)程,則接收成功;否則,接收協(xié)程進(jìn)入阻塞狀態(tài)。
那么,上面的圖可以簡(jiǎn)化成如下:
另外需要額外注意一點(diǎn),對(duì)于非緩沖區(qū)通道的發(fā)送和接收操作。如果是在main函數(shù)中進(jìn)行發(fā)送和接收,那么會(huì)造成死鎖。如下:
func main() {
var ch = make(chan int)
<-ch
fmt.Println("the End")
}
//或
func main() {
var ch = make(chan int)
ch <- 2
fmt.Println("the End")
}
所以,對(duì)于非緩沖區(qū)通道的發(fā)送和接收操作,最主要的問(wèn)題就是可能會(huì)造成阻塞。除非,兩個(gè)發(fā)送和接收協(xié)程都存在,而且要在不同的協(xié)程里。
有緩沖通道
有緩沖區(qū)通道就是在通道中有一塊緩沖區(qū),發(fā)送和接收都可以針對(duì)緩沖區(qū)進(jìn)行操作。也稱為異步發(fā)送和接收。在有緩沖通道的狀態(tài)下,j對(duì)于發(fā)送操作來(lái)說(shuō),有緩沖通道的狀態(tài)分為緩沖區(qū)滿和未滿兩種狀態(tài)。根據(jù)上面的發(fā)送流程圖來(lái)說(shuō),當(dāng)緩沖區(qū)滿了,自然就不能再發(fā)送了,就會(huì)進(jìn)入等待發(fā)送隊(duì)列。同時(shí)阻塞,等待被接收協(xié)程喚醒。
對(duì)于接收操作來(lái)說(shuō),有緩沖通道的狀態(tài)分為緩沖區(qū)空和未滿兩種狀態(tài)。同樣,如果當(dāng)緩沖區(qū)空時(shí),無(wú)數(shù)據(jù)可接收,自然就進(jìn)入到接收等待隊(duì)列。同時(shí)進(jìn)入阻塞,等待被發(fā)送協(xié)程喚醒。
已關(guān)閉狀態(tài)的通道
關(guān)閉通道是通過(guò)**close**函數(shù)進(jìn)行的。本質(zhì)上關(guān)閉一個(gè)通道,就是將通道結(jié)構(gòu)中的closed字段置為 1。從源代碼中可以獲知:
- 關(guān)閉nil通道:panic
- 關(guān)閉已經(jīng)關(guān)閉了的通道:panic。這一點(diǎn)可以這樣理解,關(guān)閉一個(gè)已經(jīng)關(guān)閉的通道是沒(méi)有任何意義的。
發(fā)送消息到已關(guān)閉的通道
給已經(jīng)關(guān)閉了的通道發(fā)送消息會(huì)引發(fā)panic。這個(gè)很好理解,因?yàn)橥ǖ酪呀?jīng)關(guān)閉,就是為了不讓發(fā)消息了。如下代碼:
從已關(guān)閉的通道接收消息
從已關(guān)閉的通道中接收消息時(shí),都能操作成功。但會(huì)根據(jù)通道中是否有元素有以下不同:
- 如果通道中已經(jīng)沒(méi)有元素了,則會(huì)返回一個(gè)false的狀態(tài)。
- 如果通道中有元素,則會(huì)繼續(xù)接收通道中的元素,直到接收完,并返回false。
你看,其實(shí)代碼也很簡(jiǎn)單。我們將代碼拆解一下,就是右側(cè)的流程圖。
nil通道
通過(guò)以下方式定義的通道類型的變量,其默認(rèn)值就是nil。
var ch chan int
nil通道相當(dāng)于沒(méi)有分配通道的底層結(jié)構(gòu)
如下是從源代碼中截取的各個(gè)操作以及對(duì)應(yīng)操作結(jié)果。通過(guò)源代碼可獲知:
- 關(guān)閉nil通道會(huì)panic
- 從nil通道接收、發(fā)送消都會(huì)阻塞
總結(jié)
golang中的通道就是用來(lái)在協(xié)程間進(jìn)行通信的。我們從源碼級(jí)別推導(dǎo)了針對(duì)通道的各個(gè)狀態(tài)下的操作所產(chǎn)生的結(jié)果。最后總結(jié)一下:緩沖區(qū)通道:
- 只要有緩沖空間就能發(fā)送成功。除非緩沖空間滿了,則產(chǎn)生阻塞。
- 只要緩沖空間中有元素就能接收成功。除非沒(méi)有元素,則產(chǎn)生阻塞。
nil通道:
- nil通道是沒(méi)有初始化底層數(shù)據(jù)結(jié)構(gòu)的通道。因?yàn)闆](méi)有空間可存儲(chǔ)任何元素,所以發(fā)送和接收都會(huì)產(chǎn)生阻塞。關(guān)閉nil通道,則會(huì)引發(fā)panic。
已關(guān)閉的通道:
- 往已關(guān)閉的通道中發(fā)送消息,會(huì)引發(fā)panic。
- 從已關(guān)閉通道中接收消息,會(huì)成功。
- 關(guān)閉已關(guān)閉的通道,也會(huì)引發(fā)panic。