前言
本文算是對Diving Deep Into The Golang Channels的翻譯,也算加強對channel的了解和使用。
使用channel
func goRoutineA(a <- chan int){
val := <- a
fmt.Println("goRoutineA received the data", val)
}
func main(){
ch := make(chan int) // 定義一個channel:接收int類型
go goRoutineA(ch)
time.Sleep(time.Second * 1) // 防止主線程退出看不到goroutine內(nèi)容輸出
}
整個執(zhí)行流程如下


從上面兩張圖片看到:使用make(chan int)定義的channel,當channel中不存在數(shù)據(jù)時 在執(zhí)行<- a時會被blocked直到channel中有數(shù)據(jù)。
在golang中使用channel能夠使得runnable的goroutine在向channel發(fā)送或接收數(shù)據(jù)時處于blocked。
channel structure
在go中,channel實現(xiàn)了在不同的goroutine間傳遞message的基本。
當我們使用make函數(shù)來創(chuàng)建channel后,對應(yīng)的結(jié)構(gòu)應(yīng)該是怎樣的?
ch := make(chan int, 3)

接下來我們會針對其中的一些內(nèi)容進行詳解
hchan struct
當我們通過make(chan int, 3)創(chuàng)建一個buffer=3的channel時,就會創(chuàng)建一個hchan結(jié)構(gòu)

- dataqsize: 對應(yīng)的channel的buffer大小,比如使用make(chan T, N),其中T代表channel中元素的類型,N就是channel的buffer大?。?/li>
- elementsize:channel中的元素大??;
- buf:channel中element真正存放的循環(huán)隊列;不過該字段只有在使用buffered的channel時才有意義;
- closed:記錄當前channel是否已關(guān)閉,在使用make創(chuàng)建一個channel后,closed=0, 代表當前channel處于open;當調(diào)用close時可將該channel關(guān)閉,closed=1;代表當前channel不能再進行任何write操作。
- recvq 和 sendq:都是等待隊列,主要存放進行讀取channel數(shù)據(jù)或?qū)懭隿hannel數(shù)據(jù)時處于blocked的goroutines。
- lock:主要用來保證channel的讀寫或發(fā)送接收是互斥操作。確保對channel的讀取或?qū)懭氲淖枞?/li>
sudog struct
可將sudgo當成goroutine來理解

先將前面的實例進行調(diào)整下:
func goRoutineA(a <- chan int){
val := <- a
fmt.Println("goroutineA received the data", val)
}
func goRoutineB(b <- chan int){
val := <- b
fmt.Println("goroutineB received the data ", val)
}
func main(){
ch := make(chan int)
go goRoutineA(ch)
go goRoutineB(ch)
ch <- 3
time.Sleep(time.Second * 1)
}
對應(yīng)的生成的channel結(jié)構(gòu)如下:

可以看到凸出部分展示了本實例中定義兩個goroutine(goroutineA和goroutineB)來嘗試讀取channel中的數(shù)據(jù)。在執(zhí)行 ch <- 3之前,由于channel中并沒有任何數(shù)據(jù),而兩個goroutine將會阻塞在接收數(shù)據(jù)操作上,并用sudog進行包裝,同時兩個sudog會被存放到recvq里。
在channel中的recvq和sendq都是基于鏈表實現(xiàn)的,如下

對于channel的sendq類似,此處不再累述。接下來看看當執(zhí)行ch <-3發(fā)生了什么?
channel之send操作: c<- x
先看看如下幾種send操作:
-
1.對nil channel執(zhí)行send操作

在對一個nil channel執(zhí)行send操作時 會導(dǎo)致當前goroutine暫停其操作
-
2.對closed channel執(zhí)行send

向一個已經(jīng)closed的channel發(fā)送數(shù)據(jù)會觸發(fā)一個panic
-
3.當一個goroutine阻塞在channel上,send數(shù)據(jù)時會直接將數(shù)據(jù)發(fā)送該goroutine

該實例也說明recvq在其中扮演一個最終的角色:若是在recvq中任意一個goroutine在等待接收數(shù)據(jù),對應(yīng)的channel的wirter會直接將value傳遞給當前的goroutine(waiting receiver)。見send函數(shù):

在396行代碼處 goready(gp, skip + 1),會使得在阻塞等待數(shù)據(jù)的那個goroutine將被再次runnable,go scheduler也將會再次運行該goroutine。
-
4.Buffered Channel
當我們通過make(chan T, N)定義一個帶有buffer的channel時,若是對應(yīng)的hchan.buf還有可用空間則會將data存到到buffer中而不是像非buffered的channel一樣處于阻塞,等待數(shù)據(jù)被接收。

chanbuf(c, i)直接訪問相應(yīng)的內(nèi)存空間。
通過對比qcount和dataqsiz來判斷hchan.buf是否還有free空間;通過將ep指針指向的區(qū)域copy到ringbuffer,來完成入列元素的send操作,并調(diào)整sendx和qcount。
-
5.若是hchan.buf沒有可用空間時 會如何???

上述代碼:
首先會在當前stack上創(chuàng)建一個goroutine,并將該goroutine狀態(tài)=park同時將該goroutine添加到sendq中。
關(guān)于send
1.將當前的channel進行blocked
2.確定執(zhí)行write,會從recvq中獲取一個等待的goroutine,并將對應(yīng)的element直接寫給該goroutine。
3.當對應(yīng)的recvq是空的,首先要確保當前的buffer是否可用,若是可用,則從當前的goroutine的copy數(shù)據(jù)到buffer中
typedmemmove內(nèi)部使用memmove將一個內(nèi)存塊從一個位置copy到另外一個位置。
4.若是buffer已滿,則寫入到channel的元素會被保存到當前運行的goroutine,并且當前goroutine將sendq處進行等待。
通過對比buffered channel和unbuffered channel差別在于對應(yīng)的hchan分配有buffer。對于一個unbuffered channel 當send數(shù)據(jù)時并沒有對應(yīng)的receiver則會將元素保存到sudog中的elem字段,對應(yīng)buffered channel也是同樣的道理。
接下來會通過結(jié)合實例來闡述關(guān)于上面羅列的第4點:
如下代碼只是用來演示 執(zhí)行可能會導(dǎo)致一個panic
package main
func goroutineA(c2 chan int){
c2 <- 2
}
func main(){
c2 := make(chan int)
go routineA(c2)
for{}
}
如上的運行時channel的結(jié)構(gòu)

不過即使我們將值2添加到channel中對應(yīng)的buf卻不存在該值,將會保存在goroutine的sudog結(jié)構(gòu)中。在上面例子中g(shù)oroutineA向channel c2發(fā)送數(shù)據(jù),但此時并沒有對應(yīng)的receiver準備接收數(shù)據(jù),因而goroutineA將被添加到channel的sendq列表中,并一直阻塞暫停等待receiver來獲取數(shù)據(jù)。接下來看看運行時的sendq結(jié)構(gòu),來驗證前面的內(nèi)容

這樣在實例代碼中 ch <- 2后具體發(fā)生的事宜。
而對于recvq來說如果存在等待狀態(tài)的goroutine,它獲取queue的第一個sudog并將數(shù)據(jù)放到goroutine中。
針對channel所有的transfer都是采用值copy的方式。也就是說在channel的所有的操作都是值拷貝。
值拷貝
正如上面演示樣例 也是通過拷貝g的值到buffer中。
Don't communicate by sharing memory; share memory by communicating.
&{Ankur 25}
modifyUser Received Value &{Ankur Anand 100}
printUser goRoutine called &{Ankur 25}
&{Anand 100}

receive channel
其實跟channel send操作很類似。

Select: 多路復(fù)用
演示實例

1.在select代碼塊中的case執(zhí)行都是互斥的,故而是需要select case中的channel來獲取lock執(zhí)行的,每個channel獲取執(zhí)行l(wèi)ock的順序是基于Hchan地址的排序來進行l(wèi)ock的獲取,這樣就能確保不會同時鎖定所有相關(guān)通道的互斥鎖。
sellock(scases, lockorder)
每個在scases數(shù)組中的scase包括當前case的操作類型以及它所在的channel。

-
kind 代表當前case的操作類型,可能取值:CaseRecv、CaseSend、CaseDefault
2.計算輪詢順序:shuffle所有涉及的通道,以提供偽隨機保證,并根據(jù)輪詢順序依次遍歷所有情況,以查看其中是否有準備好進行通信。這個輪詢順序使得select操作不必遵循程序中聲明的順序。
poll order
case in select
3.在select代碼塊中,只要有一個通道操作沒有阻塞,select語句就可以返回,如果選擇的通道已經(jīng)準備好了,甚至不需要接觸所有通道。
若是當前沒有通道響應(yīng),也沒有默認語句,則當前g必須根據(jù)情況掛載所有通道的相應(yīng)等待隊列。
若是當前所有的case都已準備好, 則會隨機執(zhí)行一個case。
park goroutine in select case - sg.isSelect 代表goroutine正在參與當前的select塊。
channel是go中一個非常強大和有趣的機制。但是為了有效地使用它們,你必須了解它們是如何工作的。希望本文能夠解釋Go中通道所涉及的非常基本的工作原理。



