Golang 設計模式: 不要通過共用記憶體來通訊,而要通過通訊實現記憶體共用
channel是基於通訊順序模型(communication sequential processes, CSP)的並行模式,可以讓一個 goroutine 傳送特定值到另一個 goroutine 的通訊機制
channel中的資料遵循先入先出(First In First Out)的規則,保證收發資料的順序
channel的原始碼在runtime包下的chan.go檔案, 參見chan.go
以下時channel的部分結構:
type hchan struct { qcount uint dataqsiz uint buf unsafe.Pointer elemsize uint16 closed uint32 elemtype *_type sendx uint recvx uint recvq waitq sendq waitq lock mutex } type waitq struct { first *sudog last *sudog }
其中:
qcount: 佇列中剩餘的元素個數 dataqsiz: 環形佇列長度,即可以存放的元素個數, make初始化時指定 buf: 快取區,實際上就是環形佇列(有環形佇列就有緩衝區,否則沒有緩衝區),指向環形佇列首部的指標,基於環形佇列實現,大小等於make初始化channel時指定的環形佇列長度,如果make初始化channel時不指定dataqsiz,則buf=0。只有緩衝型的channel才有buf elemsize: 每個元素的大小 closed: channel關閉標誌 elemtype: 元素型別 sendx: 寫入資料的索引,即從哪個位置開始寫入資料,取值[0, dataqsiz) recvx: 讀取資料的索引,即從哪個位置開始讀取資料,取值[0, dataqsiz) recvq: 接收等待佇列,連結串列結構,長度無限長, 讀取資料的goroutine等待佇列, 如果channel的緩衝區為空或者沒有緩衝區,讀取資料的goroutine被阻塞,加入到recvq等待佇列中。因讀阻塞的goroutine會被向channel寫入資料的goroutine喚醒 sendq: 傳送等待佇列,連結串列結構,長度無限長, 寫入資料的goroutine等待佇列, 如果channel的緩衝區為滿或者沒有緩衝區,寫入資料的goroutine被阻塞,加入到sendq等待佇列中。因寫阻塞的goroutine會被從channel讀取資料的goroutine喚醒 lock: 並行控制鎖, 同一時刻,只允許一個, channel不允許並行讀寫
1 結構圖
其中:
環形佇列中的0表示沒有資料,1表示有資料; G表示一個goroutine dataqsiz表示環形佇列的長度為6, 即可快取6個元素 buf指向環形佇列首部,此時還可以快取2個元素 qcount表示環形佇列中有4個元素 sendx表示下一個傳送的資料在環形佇列index=5的位置寫入,取值[0, 6) recvx表示從環形佇列index=1的位置讀取資料,取值[0, 6) sendq, recvq: 虛線表示,此時轉態下的channel可能有等待佇列
1 宣告channel型別
//同時讀寫的channel var 變數 chan 型別 //只能寫入資料的channel var 變數 chan<- 型別 //只能讀取資料的channel var 變數 <-chan 型別
其中:
型別:channel內的資料型別,golang支援的合法型別
宣告的channel此時還是nil,需要配合make函數初始化之後才能使用
2 建立channel
//無緩衝的channel 變數 := make(chan 資料型別) //有緩衝的channel 變數 := make(chan 資料型別, dataqsiz)
1 傳送資料的格式
變數 <- 值
2 寫資料的過程
1) 流程圖如下:
其中:
G表示一個goroutine 虛線表示sendq中堵塞的G被喚醒的流程,如果G沒有被喚醒,則一直堵塞下去,此時關閉channel,會觸發panic
2) 過程描述:
1) 如果channel是nil(沒有初始化), 傳送資料則一直會堵塞,這是一個BUG 2) 如果等待接收佇列recvq 不為空,說明沒有緩衝區或者緩衝區沒有資料,直接從recvq取出一個G資料寫入,把G喚醒,結束傳送過程 3) 如果等待接收佇列recvq為空,且緩衝區有空位,那麼就直接將資料寫入緩衝區sendx位置, sendx++, qcount++, 結束傳送過程 4) 如果等待接收佇列recvq為空,緩衝區沒有空位,將資料寫入G,然後把G放到等待傳送佇列sendq中進行阻塞,等待被喚醒, 結束傳送過程。當被喚醒的時候,需要寫入的資料已經被讀取出來,且已經完成了寫入操作
1 接收資料的格式
1) 阻塞接收資料
程式阻塞直到收到資料並賦值
data := <-ch
2) 非阻塞接收資料
非阻塞的通道接收方法可能造成高的 CPU 佔用
//ok表示是否接收到資料 data, ok := <-ch
3) 接收資料並忽略
程式阻塞直到接收到資料,但接收到的資料會被忽略
<-ch
4) 迴圈接收
channel是可以進行遍歷的,遍歷的結果就是接收到的資料
for data := range ch { //done }
5) SELECT語句接收
select 的特點是隻要其中有一個 case 已經完成,程式就會繼續往下執行,而不會考慮其他 case 的情況
在一個 select 語句中,Go語言會按順序從頭至尾評估每一個傳送和接收的語
如果其中的多條case語句可繼續執行(即沒有被阻塞),那麼就從這些case語句中任意選擇一條
如果沒有case語句可以執行(即所有的通道都被阻塞):
1) 如果有 default 語句,執行 default 語句,同時程式的執行會從 select 語句後的語句中恢復
2) 如果沒有 default 語句,那麼 select 語句將被阻塞,直到至少有一個case可以進行下去
select { case <- chan1: //done case chan2 <- 2: //done default: //done }
2 讀取資料的流程
1) 流程圖如下:
其中:
G表示一個goroutine 虛線表示recvq中堵塞的G被喚醒的流程,如果G沒有被喚醒,則一直堵塞下去,此時關閉channel,會得到channel型別的零值
2) 過程描述:
1 如果等待傳送佇列sendq不為空,且沒有緩衝區,直接從sendq中取出G,讀取資料,最後把G喚醒,結束讀取過程 2 如果等待傳送佇列sendq不為空,有緩衝區(此時緩衝區滿了),從緩衝區中首部讀出資料,把sendq出列的G中資料寫入緩衝區尾部,把G喚醒,結束讀取過程 3 如果等待傳送佇列sendq為空,且環形佇列無元素,將goruntime加入等待接收佇列recvq中進行堵塞,等待被喚醒 4 如果等待傳送佇列sendq為空,環形佇列有元素,直接從緩衝區讀取資料,結束讀取過程
1 格式
close(ch)
2 過程描述
1) 首先校驗chan是否已被初始化,然後加鎖之後再校驗是否已被關閉過,如果校驗都通過了,那麼將closed欄位設值為1 2) 遍歷recvq和sendq,並將所有的goroutine 加入到glist中 3) 將所有glist中的goroutine加入排程佇列,等待被喚醒 4) recvq中的goroutine接收到對應資料的零值,sendq中的goroutine會直接panic
1 向一個nil的channel傳送/讀取資料會一直堵塞下去?該如何喚醒?
會一直堵塞下去,不會被喚醒,可能會造成洩露,這是一個BUG
2 等待傳送佇列(sendq)中有資料,如果一直沒有gouruntine從channel裡面讀資料會不會造成洩漏?
會造成洩露,channel用完了,最好要close
3 向已經關閉的channel讀/寫資料會發生什麼?
寫已經關閉的 channel 會觸發panic
讀已經關閉的 channel,能一直讀到資料:
1) 如果 channel 關閉前,buf內有元素還未讀,會正確讀到 channel 內的值,且返回的第二個 bool 值為 true
2) 如果 channel 關閉前,buf內有元素已經被讀完,channel 內無值,返回 channel 元素的零值,第二個 bool 值為 false
4 觸發 panic 的三種情況
1) 向一個關閉的 channel 進行寫操作
2) 關閉一個為 nil 的 channel
3) 重複關閉一個 channel