通道是一個goroutine之間很關鍵的通訊媒介。
理解golang的通道很重要,這裡記錄平時易忘記的、易混淆的點。
1. 基本使用
剛宣告的通道,零值為nil,無法直接使用,需配合make函式進行初始化
ic := make(chan int)
ic <-22 // 向無緩衝通道寫入資料
v := <-ic // 從無緩衝通道讀取資料
- 無緩衝通道: 一手交錢,一手交貨, sender、receiver必須同時做好動作,才能完成傳送->接收;否則,先準備好的一方將會阻塞等待。
- 有緩衝通道 make(chan int,10):滑軌流水線,因為存在緩衝空間,故並不強制sender、receiver必須同時準備好;當通道空或滿時, 一方會阻塞。
通道存在三種狀態: nil, active, closed
針對這三種狀態,sender、receiver有一些行為,我也不知道如何強行記憶這些行為 ☹️:
動作 | nil | active | closed |
---|---|---|---|
close | panic | 成功 | panic |
ch <- | 死鎖 | 阻塞或成功 | panic |
<-ch | 死鎖 | 阻塞或成功 | 零值 |
2. 從1個例子看chan的實質
package main
import (
"fmt"
)
func SendDataToChannel(ch chan int, value int) {
fmt.Printf("ch's value:%v, chan's type: %T \n", ch, ch) // %v 顯示struct的值;%T 顯示型別
ch <- value
}
func main() {
var v int
ch := make(chan int)
fmt.Printf("ch's value:%v, chan's type: %T \n", ch, ch)
go SendDataToChannel(ch, 101) // 通過通道傳送資料
v = <-ch // 從通道接受資料
fmt.Println(v) // 101
}
能正確列印101。
Q1: 剛學習golang的時候,一直給我們灌輸golang函式是值傳遞,那上例在另外一個協程內部對形參的操作,為什麼會影響外部的實參?
請關注格式化字元的日誌輸出:
ch's value:0xc000018180, chan's type: chan int
ch's value:0xc000018180, chan's type: chan int
101
A: 上面的日誌顯示傳遞的ch
是一個指標值0xc000018180,型別是chan int
( 這並不是說ch是指向chan int
型別的指標)。
chan int
本質就是指向hchan結構體的指標。
內建函式make建立通道: func makechan(t *chantype, size int) *hchan
返回了指向hchan結構體
的指標:
type hchan struct {
qcount uint // 佇列中已有的快取元素的長度
dataqsiz uint // 環形佇列的長度
buf unsafe.Pointer // 環形佇列的地址
elemsize uint16
closed uint32
elemtype *_type // 元素型別
sendx uint // 待傳送的元素索引
recvx uint // 待接受元素索引
recvq waitq // 阻塞等待的goroutine
sendq waitq // 阻塞等待的gotoutine
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
Q2: 緩衝通道內部為什麼要使用環形佇列?
A:golang是使用陣列來實現通道佇列,在不移動元素的情況下, 佇列會出現“假滿”的情況,
在做成環形佇列的情況下, 所有的入隊出隊操作依舊是 O(1)的時間複雜度,同時元素空間可以重複利用。
需要使用sendIndex,receIndex來標記實際的待插入/拉取位置,顯而易見會出現 sendIndex<=receIndex 的情況。
recvq
,receq
是由連結串列實現的佇列,用於儲存阻塞等待的goroutine和待傳送/待接收值,
這兩個結構也是阻塞goroutine被喚醒的準備條件。
3. 傳送/接收的細節
① 不要使用共享記憶體來通訊,而是使用通訊來共享記憶體
元素值從外界進入通道會被複制,也就是說進入通道的是元素值的副本,並不是元素本身進入通道 (出通道類似)。
金玉良言落到實處:不同的執行緒不共享記憶體、不用鎖,執行緒之間通訊用channel同步也用channel。
傳送/接收資料的兩個動作(G1,G2,G3)沒有共享的記憶體,底層通過hchan結構體的buf,使用copy記憶體的方式進行通訊,最後達到了共享記憶體的目的。
② 根據第①點,傳送操作包括:複製待傳送值,放置到通道內;
接收操作包括:複製元素值, 放置副本到接收方,刪除原值,以上行為在全部完成之前都不會被打斷。
所以第①點所說的無鎖,其實指的業務程式碼無鎖,通道底層實現還是靠鎖。
以send操作為例,下面程式碼擷取自 https://github.com/golang/go/blob/master/src/runtime/chan.go#L216
if c.qcount < c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
qp := chanbuf(c, c.sendx) // 計算出buf中待插入位置的地址
if raceenabled {
racenotify(c, c.sendx, nil)
}
typedmemmove(c.elemtype, qp, ep) // 將元素copy進指定的qp地址
c.sendx++ // 重新計算待插入位置的索引
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
一個常規的send動作:
- 計算環形佇列的待插入位置的地址
- 將元素copy進指定的qp地址
- 重新計算待插入位置的索引sendx
- 如果待插入位置==佇列長度,說明插入位置已到尾部,需要插入首部。
- 以上動作加鎖
③ 進入等待狀態的goroutine會進入hchan的sendq/recvq列表
排程器將G1、G2置為waiting狀態,G1、G2進入sendq列表,同時與邏輯處理器分離;
直到有G3嘗試讀取通道內recvx
元素,之後將喚醒隊首G1進入runnable狀態,加入排程器的runqueue。
這裡面涉及gopark
, goready
兩個函式。
如果是無緩衝通道引起的阻塞,將會直接拷貝G1的待傳送值到G2的儲存位置
✍️ https://github.com/golang/go/blob/master/src/runtime/chan.go#L527
package main
import (
"fmt"
"time"
)
func SendDataToChannel(ch chan int, value int) {
time.Sleep(time.Millisecond * time.Duration(value))
ch <- value
}
func main() {
var v int
var ch chan int = make(chan int)
go SendDataToChannel(ch, 104) // 通過通道傳送資料
go SendDataToChannel(ch, 100) // 通過通道傳送資料
go SendDataToChannel(ch, 95) // 通過通道傳送資料
go SendDataToChannel(ch, 120) // 通過通道傳送資料
time.Sleep(time.Second)
v = <-ch // 從通道接受資料
fmt.Println(v)
time.Sleep(time.Second * 10)
}
Q3:上述程式碼大概率穩定輸出95
。
A:雖然4個goroutine被啟動的順序不定,但是肯定都阻塞了,阻塞的時機不一樣,被喚醒的是sendq
隊首的goroutine,基本可認為第三個goroutine被首先捕獲進sendq
,因為是無緩衝通道,將會直接拷貝G3的95給到待接收地址。
4. 業內總結的通道的常規姿勢
無緩衝、緩衝通道的特徵,已經在golang領域形成了特定的套路。
-
當容量為0時,說明通道中不能存放資料,在傳送資料時,必須要求立馬有人接收,此時的通道稱之為無緩衝通道。
-
當容量為1時,說明通道只能快取一個資料,若通道中已有一個資料,此時再往裡傳送資料,會造成程式阻塞,利用這點可以利用通道來做鎖。
-
當容量大於1時,通道中可以存放多個資料,可以用於多個協程之間的通訊管道,共享資源。
Q4: 為什麼無緩衝通道不適合做鎖?
A: 我們先思考一下鎖的業務實質: 獲取獨佔標識,並能夠繼續執行; 無緩衝通道雖然可以獲取獨佔標識,但是他阻塞了自身goroutine的執行,所以並不適合實現業務鎖。