目錄
- 前言
- 一、Channel的定義
- 二、Channel的操作
- 三、Channel發送和接收操作的特點
- 四、Channel的類型
- 五、Channel的源碼學習
- 總結
前言
先來看一道面試題:
對已經關閉的 chan 進行讀寫,會怎么樣?為什么?
在上一篇學習 Go 協程的文章中,知道 go 關鍵字可以用來開啟一個 goroutine 進行任務處理,但多個任務之間如果需要通信,就需要用到通道(channel)了。
一、Channel的定義
聲明并初始化一個通道,可以使用 Go 語言的內建函數 make,同時指定該通道類型的元素類型,下面聲明了一個 chan int 類型的 channel:
二、Channel的操作
發送(寫):發送操作包括了“復制元素值”和“放置副本到通道內部”這兩個步驟。即:進入通道的并不是操作符右邊的那個元素值,而是它的副本。
ch := make(chan int)
// write to channel
ch - x
接收(讀):接收操作包含了“復制通道內的元素值”、“放置副本到接收方”、“刪掉原值”三個步驟。
ch := make(chan int)
// read from channel
x - ch
// another way to read
x = - ch
關閉:關閉 channel 會產生一個廣播機制,所有向 channel 讀取消息的 goroutine 都會收到消息。
ch := make(chan int)
close(ch)
從一個已關閉的 channel 中讀取消息永遠不會阻塞,并且會返回一個為 false 的 ok-idiom,可以用它來判斷 channel 是否關閉:
如果 ok 是false,表明接收的 v 是產生的零值,這個 channel 被關閉了或者為空。
三、Channel發送和接收操作的特點
- 一個通道相當于一個先進先出(FIFO)的隊列:也就是說,通道中的各個元素值都是嚴格地按照發送的順序排列的,先被發送通道的元素值一定會先被接收。
- 對于同一個通道,發送操作之間和接收操作之間是互斥的:同一時刻,對同一通道發送多個元素,直到這個元素值被完全復制進該通道之后,其他針對該通道的發送操作才可能被執行。接收也是如此。
- 發送操作和接收操作中,對元素值的處理是不可分割的:前面我們知道發送一個值到通道,是先復制值,再將該副本移動到通道內部,“不可分割”指的是發送操作要么還沒復制元素值,要么已經復制完畢,絕不會出現只復制了一部分的情況。接收也是同理,在準備好元素值的副本之后,一定會刪除掉通道中的原值,絕不會出現通道中仍有殘留的情況。
- 發送操作和接收操作在完全完成之前會被阻塞:發送操作包括了“復制元素值”和“放置副本到通道內部”這兩個步驟。在這兩個步驟完全完成之前,發起這個發送操作的那句代碼會一直阻塞在那里,在它之后的代碼不會有執行的機會,直到阻塞解除。
四、Channel的類型
channel 分為不帶緩存的 channel 和帶緩存的 channel。
使用 make 聲明一個通道類型變量時,除了指定通道的元素類型,還可以指定通道的容量,也就是通道最多可以緩存多少個元素值,當容量為 0 時,該通道為非緩沖通道,當容量大于 0 時,該通道為帶有緩沖的通道。
ch := make(chan int) //無緩沖的channel
ch := make(chan int, 3) //帶緩沖的channel
非緩沖通道和緩沖通道有著不同的數據傳遞方式:
- 非緩沖通道:無論是發送操作還是接收操作,一開始執行就會被阻塞,直到配對的操作也開始執行,才會繼續傳遞。即:只有收發雙方對接上了,數據才會被傳遞。數據直接從發送方復制到接收方。非緩沖通道傳遞數據的方式是同步的。
- 緩沖通道:如果通道已滿,對它的所有發送操作都會被阻塞,直到通道中有元素值被接收走。反之,如果通道已空,那么對它的所有接收操作都會被阻塞,直到通道中有新的元素值出現。元素值會先從發送方復制到緩沖通道,之后再由緩沖通道復制給接收方。緩沖通道傳遞數據的方式是異步的。
五、Channel的源碼學習
Channel 的主要實現在 src/runtime/chan.go 中,go 版本為 go version go1.14.6 darwin/amd64這里主要看 chansend 如何實現的。
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
if debugChan {
print("chansend: chan=", c, "\n")
}
if raceenabled {
racereadpc(c.raceaddr(), callerpc, funcPC(chansend))
}
// Fast path: check for failed non-blocking operation without acquiring the lock.
//
// After observing that the channel is not closed, we observe that the channel is
// not ready for sending. Each of these observations is a single word-sized read
// (first c.closed and second c.recvq.first or c.qcount depending on kind of channel).
// Because a closed channel cannot transition from 'ready for sending' to
// 'not ready for sending', even if the channel is closed between the two observations,
// they imply a moment between the two when the channel was both not yet closed
// and not ready for sending. We behave as if we observed the channel at that moment,
// and report that the send cannot proceed.
//
// It is okay if the reads are reordered here: if we observe that the channel is not
// ready for sending and then observe that it is not closed, that implies that the
// channel wasn't closed during the first observation.
if !block c.closed == 0 ((c.dataqsiz == 0 c.recvq.first == nil) ||
(c.dataqsiz > 0 c.qcount == c.dataqsiz)) {
return false
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
lock(c.lock)
if c.closed != 0 {
unlock(c.lock)
panic(plainError("send on closed channel"))
}
if sg := c.recvq.dequeue(); sg != nil {
// Found a waiting receiver. We pass the value we want to send
// directly to the receiver, bypassing the channel buffer (if any).
send(c, sg, ep, func() { unlock(c.lock) }, 3)
return true
}
if c.qcount c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
qp := chanbuf(c, c.sendx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
}
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(c.lock)
return true
}
if !block {
unlock(c.lock)
return false
}
// Block on the channel. Some receiver will complete our operation for us.
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// No stack splits between assigning elem and enqueuing mysg
// on gp.waiting where copystack can find it.
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg)
gopark(chanparkcommit, unsafe.Pointer(c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
// Ensure the value being sent is kept alive until the
// receiver copies it out. The sudog has a pointer to the
// stack object, but sudogs aren't considered as roots of the
// stack tracer.
KeepAlive(ep)
// someone woke us up.
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
gp.activeStackChans = false
if gp.param == nil {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
releaseSudog(mysg)
return true
}
從代碼中可以看到:
- 有 goroutine 阻塞在 channel recv 隊列上,此時緩存隊列為空,直接將消息發送給 reciever goroutine,只產生一次復制。
- 當 channel 緩存隊列有剩余空間時,將數據放到隊列里,等待接收,接收后總共產生兩次復制。
- 當 channel 緩存隊列已滿時,將當前 goroutine 加入 send 隊列并阻塞。
所以,開頭的面試題就有了答案:
讀:
讀已經關閉的 chan,能一直讀到內容,但是讀到的內容根據通道內關閉前是否有元素而不同。
如果 chan 關閉前,buffer 內有元素還未讀,會正確讀到 chan 內的值,且返回的第二個 bool 值為 true;
如果 chan 關閉前,buffer 內有元素已經被讀完,chan 內無值,返回 channel 元素的零值,第二個 bool 值為 false。
寫:
寫已經關閉的 chan 會 panic。
總結
到此這篇關于Go中Channel發送和接收操作的文章就介紹到這了,更多相關Go Channel發送和接收內容請搜索腳本之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持腳本之家!
您可能感興趣的文章:- golang判斷chan channel是否關閉的方法
- Golang中channel使用的一些小技巧
- Go語言的管道Channel用法實例
- Golang優雅關閉channel的方法示例
- golang中單向channel的語法介紹
- Go語言中 Channel 詳解
- 淺談Go Channel 高級實踐