Go的Channel傳送和接收

鹿呦呦發表於2021-08-24

先來看一道面試題:

對已經關閉的 chan 進行讀寫,會怎麼樣?為什麼?

在上一篇學習 Go 協程的文章中,知道 go 關鍵字可以用來開啟一個 goroutine 進行任務處理,但多個任務之間如果需要通訊,就需要用到通道(channel)了。

一、Channel的定義

宣告並初始化一個通道,可以使用 Go 語言的內建函式 make,同時指定該通道型別的元素型別,下面宣告瞭一個 chan int 型別的 channel:

ch := make(chan int)

二、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 是否關閉:

v, ok := <-ch

如果 ok 是false,表明接收的 v 是產生的零值,這個 channel 被關閉了或者為空。

三、Channel傳送和接收操作的特點

  1. 一個通道相當於一個先進先出(FIFO)的佇列:也就是說,通道中的各個元素值都是嚴格地按照傳送的順序排列的,先被髮送通道的元素值一定會先被接收。

  2. 對於同一個通道,傳送操作之間和接收操作之間是互斥的:同一時刻,對同一通道傳送多個元素,直到這個元素值被完全複製進該通道之後,其他針對該通道的傳送操作才可能被執行。接收也是如此。

  3. 傳送操作和接收操作中,對元素值的處理是不可分割的:前面我們知道傳送一個值到通道,是先複製值,再將該副本移動到通道內部,“不可分割”指的是傳送操作要麼還沒複製元素值,要麼已經複製完畢,絕不會出現只複製了一部分的情況。接收也是同理,在準備好元素值的副本之後,一定會刪除掉通道中的原值,絕不會出現通道中仍有殘留的情況。

  4. 傳送操作和接收操作在完全完成之前會被阻塞:傳送操作包括了“複製元素值”和“放置副本到通道內部”這兩個步驟。在這兩個步驟完全完成之前,發起這個傳送操作的那句程式碼會一直阻塞在那裡,在它之後的程式碼不會有執行的機會,直到阻塞解除。

四、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。

相關文章