Go中Channel發(fā)送和接收操作指南
前言
先來看一道面試題:
對已經(jīng)關(guān)閉的 chan 進(jìn)行讀寫,會怎么樣?為什么?
在上一篇學(xué)習(xí) Go 協(xié)程的文章中,知道 go 關(guān)鍵字可以用來開啟一個 goroutine 進(jìn)行任務(wù)處理,但多個任務(wù)之間如果需要通信,就需要用到通道(channel)了。
一、Channel的定義
聲明并初始化一個通道,可以使用 Go 語言的內(nèi)建函數(shù) make,同時指定該通道類型的元素類型,下面聲明了一個 chan int 類型的 channel:
ch := make(chan int)
二、Channel的操作
發(fā)送(寫):發(fā)送操作包括了“復(fù)制元素值”和“放置副本到通道內(nèi)部”這兩個步驟。即:進(jìn)入通道的并不是操作符右邊的那個元素值,而是它的副本。
ch := make(chan int) // write to channel ch <- x
接收(讀):接收操作包含了“復(fù)制通道內(nèi)的元素值”、“放置副本到接收方”、“刪掉原值”三個步驟。
ch := make(chan int) // read from channel x <- ch // another way to read x = <- ch
關(guān)閉:關(guān)閉 channel 會產(chǎn)生一個廣播機(jī)制,所有向 channel 讀取消息的 goroutine 都會收到消息。
ch := make(chan int) close(ch)
從一個已關(guān)閉的 channel 中讀取消息永遠(yuǎn)不會阻塞,并且會返回一個為 false 的 ok-idiom,可以用它來判斷 channel 是否關(guān)閉:
v, ok := <-ch
如果 ok 是false,表明接收的 v 是產(chǎn)生的零值,這個 channel 被關(guān)閉了或者為空。
三、Channel發(fā)送和接收操作的特點(diǎn)
- 一個通道相當(dāng)于一個先進(jìn)先出(FIFO)的隊列:也就是說,通道中的各個元素值都是嚴(yán)格地按照發(fā)送的順序排列的,先被發(fā)送通道的元素值一定會先被接收。
- 對于同一個通道,發(fā)送操作之間和接收操作之間是互斥的:同一時刻,對同一通道發(fā)送多個元素,直到這個元素值被完全復(fù)制進(jìn)該通道之后,其他針對該通道的發(fā)送操作才可能被執(zhí)行。接收也是如此。
- 發(fā)送操作和接收操作中,對元素值的處理是不可分割的:前面我們知道發(fā)送一個值到通道,是先復(fù)制值,再將該副本移動到通道內(nèi)部,“不可分割”指的是發(fā)送操作要么還沒復(fù)制元素值,要么已經(jīng)復(fù)制完畢,絕不會出現(xiàn)只復(fù)制了一部分的情況。接收也是同理,在準(zhǔn)備好元素值的副本之后,一定會刪除掉通道中的原值,絕不會出現(xiàn)通道中仍有殘留的情況。
- 發(fā)送操作和接收操作在完全完成之前會被阻塞:發(fā)送操作包括了“復(fù)制元素值”和“放置副本到通道內(nèi)部”這兩個步驟。在這兩個步驟完全完成之前,發(fā)起這個發(fā)送操作的那句代碼會一直阻塞在那里,在它之后的代碼不會有執(zhí)行的機(jī)會,直到阻塞解除。
四、Channel的類型
channel 分為不帶緩存的 channel 和帶緩存的 channel。
使用 make 聲明一個通道類型變量時,除了指定通道的元素類型,還可以指定通道的容量,也就是通道最多可以緩存多少個元素值,當(dāng)容量為 0 時,該通道為非緩沖通道,當(dāng)容量大于 0 時,該通道為帶有緩沖的通道。
ch := make(chan int) //無緩沖的channel ch := make(chan int, 3) //帶緩沖的channel
非緩沖通道和緩沖通道有著不同的數(shù)據(jù)傳遞方式:
- 非緩沖通道:無論是發(fā)送操作還是接收操作,一開始執(zhí)行就會被阻塞,直到配對的操作也開始執(zhí)行,才會繼續(xù)傳遞。即:只有收發(fā)雙方對接上了,數(shù)據(jù)才會被傳遞。數(shù)據(jù)直接從發(fā)送方復(fù)制到接收方。非緩沖通道傳遞數(shù)據(jù)的方式是同步的。
- 緩沖通道:如果通道已滿,對它的所有發(fā)送操作都會被阻塞,直到通道中有元素值被接收走。反之,如果通道已空,那么對它的所有接收操作都會被阻塞,直到通道中有新的元素值出現(xiàn)。元素值會先從發(fā)送方復(fù)制到緩沖通道,之后再由緩沖通道復(fù)制給接收方。緩沖通道傳遞數(shù)據(jù)的方式是異步的。
五、Channel的源碼學(xué)習(xí)
Channel 的主要實現(xiàn)在 src/runtime/chan.go 中,go 版本為 go version go1.14.6 darwin/amd64這里主要看 chansend 如何實現(xiàn)的。
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 隊列上,此時緩存隊列為空,直接將消息發(fā)送給 reciever goroutine,只產(chǎn)生一次復(fù)制。
- 當(dāng) channel 緩存隊列有剩余空間時,將數(shù)據(jù)放到隊列里,等待接收,接收后總共產(chǎn)生兩次復(fù)制。
- 當(dāng) channel 緩存隊列已滿時,將當(dāng)前 goroutine 加入 send 隊列并阻塞。
所以,開頭的面試題就有了答案:
讀:
讀已經(jīng)關(guān)閉的 chan,能一直讀到內(nèi)容,但是讀到的內(nèi)容根據(jù)通道內(nèi)關(guān)閉前是否有元素而不同。
如果 chan 關(guān)閉前,buffer 內(nèi)有元素還未讀,會正確讀到 chan 內(nèi)的值,且返回的第二個 bool 值為 true;
如果 chan 關(guān)閉前,buffer 內(nèi)有元素已經(jīng)被讀完,chan 內(nèi)無值,返回 channel 元素的零值,第二個 bool 值為 false。
寫:
寫已經(jīng)關(guān)閉的 chan 會 panic。
總結(jié)
到此這篇關(guān)于Go中Channel發(fā)送和接收操作的文章就介紹到這了,更多相關(guān)Go Channel發(fā)送和接收內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go?iota關(guān)鍵字與枚舉類型實現(xiàn)原理
這篇文章主要介紹了Go?iota關(guān)鍵字與枚舉類型實現(xiàn)原理,iota是go語言的常量計數(shù)器,只能在常量的表達(dá)式中使用,更多相關(guān)內(nèi)容需要的小伙伴可以參考一下2022-07-07Go語言for range(按照鍵值循環(huán))遍歷操作
這篇文章主要介紹了Go語言for range(按照鍵值循環(huán))遍歷操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12go語言阻塞函數(shù)和非阻塞函數(shù)實現(xiàn)
本文主要介紹了go語言阻塞函數(shù)和非阻塞函數(shù)實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03