這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
本文會嘗試解釋 go runtime 中 channel 和 select 的具體實現,部分內容來自 gophercon2017。Go版本為1.8.3
channel
第一部分講述一下 channel 的用法。channel 可以看做一個隊列,用於多個goroutine之間的通訊,例如下面的例子,一個goroutine發送msg,另一個msg接受訊息。channel 分為帶緩衝和不帶緩衝,差別不是很大,具體請自行google。看一個簡單的例子,瞭解一下channel的使用。
package mainimport "fmt"func main() { // Create a new channel with `make(chan val-type)`. // Channels are typed by the values they convey. messages := make(chan string) // Send a value into a channel using the `channel <-` // syntax. Here we send `"ping"` to the `messages` // channel we made above, from a new goroutine. go func() { messages <- "ping" }() // The `<-channel` syntax receives a value from the // channel. Here we'll receive the `"ping"` message // we sent above and print it out. msg := <-messages fmt.Println(msg)}
channel的功能點:
- 隊列
- 阻塞
- 當一端阻塞,可以被另一個端喚醒
我們圍繞這3點功能展開,講講具體的實現。
channel結構
注釋標註了幾個重要的變數,從功能上大致可以分為兩個功能單元,一個是 ring buffer,用於存資料; 一個是存放 goroutine 的隊列。
type hchan struct { qcount uint // 當前隊列中的元素個數 dataqsiz uint // 緩衝隊列的固定大小 buf unsafe.Pointer // 緩衝數組 elemsize uint16 closed uint32 elemtype *_type // element type sendx uint // 下一次發送的 index recvx uint // 下一次接收的 index recvq waitq // 接受者隊列 sendq waitq // 寄件者隊列 // 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}
Ring Buffer
主要是以下變數組成的功能, 一個 buf 儲存實際資料,兩個指標分別代表發送,接收的索引位置,配合 size, count 在數組大小範圍內來回滑動。
qcount uint // 當前隊列中的元素個數dataqsiz uint // 緩衝隊列的固定大小buf unsafe.Pointer // 緩衝數組sendx uint // 下一次發送的 indexrecvx uint // 下一次接收的 index
舉個例子,假設我們初始化了一個帶緩衝的channel, ch := make(chan int, 3), 那麼它初始狀態的值為:
qcount = 0dataqsiz = 3buf = [3]int{0, 0, 0} // 表示長度為3的數組sendx = 0recvx = 0
第一步,向 channel 裡 send 一個值, ch <- 1, 因為現在緩衝還沒滿,所以操作後狀態如下:
qcount = 1dataqsiz = 3buf = [3]int{1, 0, 0} // 表示長度為3的數組sendx = 1recvx = 0
快進兩部,連續向 channel 裡 send 兩個值 (2, 3),狀態如下:
qcount = 3dataqsiz = 3buf = [3]int{1, 2, 3} // 表示長度為3的數組sendx = 0 // 下一個發送的 index 回到了0recvx = 0
從 channel 中 receive 一個值, <- ch, 狀態如下:
qcount = 2dataqsiz = 3buf = [3]int{1, 2, 3} // 表示長度為3的數組sendx = 0 // 下一個發送的 index 回到了0recvx = 1 // 下一個接收的 index
阻塞
我們看下,如果 receive channel 時,channel 的 buffer中沒有資料是怎麼處理的。邏輯在 chanrecv 這個方法中,它的大致流程如下,僅保留了阻塞操作的代碼。
func chanrecv(t *chantype, c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) { // 檢查 channdel 是否為 nil // 當不阻塞時,檢查buffer大小,當前大小,檢查chennel是否關閉,看看是否能直接返回 // 檢查發送端是否有等待的goroutine,下部分會提到 // 當前buffer中有資料,則嘗試取出。 // 如果非阻塞,直接返回 // 沒有sender等待,buffer中沒有資料,則阻塞等待。 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 gp.waiting = mysg mysg.g = gp mysg.selectdone = nil mysg.c = c gp.param = nil c.recvq.enqueue(mysg) //關鍵操作:設定 goroutine 狀態為 waiting, 把 G 和 M 分離 goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv, 3) // someone woke us up // 被喚醒,清理 sudog if mysg != gp.waiting { throw("G waiting list is corrupted") } gp.waiting = nil if mysg.releasetime > 0 { blockevent(mysg.releasetime-t0, 2) } closed := gp.param == nil gp.param = nil mysg.c = nil releaseSudog(mysg) return true, !closed}
這裡的操作就是 建立一個 當前 goroutine 的 sudog, 然後把這個 sudog 放入 channel 的接受者等待隊列;設定當前 G 的狀態,和 M分離,到這裡當前G就阻塞了,代碼不會執行下去。
當被喚醒後,執行sudog的清理操作。這裡接受buffer中的值的指標是 ep 這個變數,被喚醒後好像沒有向 ep 中賦值的操作。這個我們下部分會講。
sudog
還剩最後一個疑問,當一個goroutine因為channel阻塞,另一個goroutine是如何喚醒它的。
channel 中有兩個 waitq 類型的變數, 看下結構發現,就是sudog的鏈表,關鍵是 sudog。sudog中包含了goroutine的引用,注意一下 elem這個變數,注釋說可能會指向stack。
type waitq struct { first *sudog last *sudog}type sudog struct { // The following fields are protected by the hchan.lock of the // channel this sudog is blocking on. shrinkstack depends on // this. g *g selectdone *uint32 // CAS to 1 to win select race (may point to stack) next *sudog prev *sudog elem unsafe.Pointer // data element (may point to stack) // The following fields are never accessed concurrently. // waitlink is only accessed by g. acquiretime int64 releasetime int64 ticket uint32 waitlink *sudog // g.waiting list c *hchan // channel}
講阻塞部分的時候,我們看到goroutine被調度之前,有一個 enqueue操作,這時,當前G的sudog已經被存入recvq中,我們看下寄件者這時的操作。
這裡的操作是,sender發送的值 直接被拷貝到 sudog.elem 了。然後喚醒 sudog.g ,這樣對面的receiver goroutine 就被喚醒了。具體請下面的注釋。
func chansend(t *chantype, c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { // 檢查工作 // 如果能從 chennel 的 recvq 彈出 sudog, 那麼直接send 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) }) return true } // buffer有空餘空間,返回; 阻塞操作}func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func()) { // 處理 index // 關鍵 if sg.elem != nil { // 這裡是根據 elemtype.size 複製記憶體 sendDirect(c.elemtype, sg, ep) sg.elem = nil } // 一些處理 // 重新設定 goroutine 的狀態,喚醒它 goready(gp, 4)}func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) { // src is on our stack, dst is a slot on another stack. // Once we read sg.elem out of sg, it will no longer // be updated if the destination's stack gets copied (shrunk). // So make sure that no preemption points can happen between read & use. dst := sg.elem typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size) memmove(dst, src, t.size)}// memmove copies n bytes from "from" to "to".// in memmove_*.s//go:noescapefunc memmove(to, from unsafe.Pointer, n uintptr)
select
在看 chanrecv()方法 時,發現了一個 block 參數,代表操作是否阻塞。一般情況下,channel 都是阻塞的(不考慮buffer),那什麼時候非阻塞呢?
第一個想到的就是 select, 在寫了default case的時候,其他的channel是非阻塞的。
還有一個可能不常用,就是 channel 的反射 value, 可以是非阻塞的,這個方法是public的,我們先看下簡單的。
func (v Value) TryRecv() (x Value, ok bool)func (v Value) TrySend(x Value) bool
select 就複雜一點點,首先在源碼中發現一段注釋:
// compiler implements//// select {// case c <- v:// ... foo// default:// ... bar// }//// as//// if selectnbsend(c, v) {// ... foo// } else {// ... bar// }//func selectnbsend(t *chantype, c *hchan, elem unsafe.Pointer) (selected bool) { return chansend(t, c, elem, false, getcallerpc(unsafe.Pointer(&t)))}// compiler implements//// select {// case v = <-c:// ... foo// default:// ... bar// }//// as//// if selectnbrecv(&v, c) {// ... foo// } else {// ... bar// }//func selectnbrecv(t *chantype, elem unsafe.Pointer, c *hchan) (selected bool) { selected, _ = chanrecv(t, c, elem, false) return}
如果是一個 case + default 的模式,那麼編譯器就調用以上方法來實現。
如果是多個 case + default 的模式呢?select 在runtime到底是如何執行的?寫個簡單的select編譯一下。
package mainfunc main() { var ch chan int select { case <-ch: case ch <- 1: default: }}
go tool compile -S -l -N test.go > test.s 結果中找一下關鍵字,例如:
0x008c 00140 (test.go:5) CALL runtime.newselect(SB)0x00ad 00173 (test.go:6) CALL runtime.selectrecv(SB)0x00ec 00236 (test.go:7) CALL runtime.selectsend(SB)0x0107 00263 (test.go:8) CALL runtime.selectdefault(SB)0x0122 00290 (test.go:5) CALL runtime.selectgo(SB)
這裡 selectgo 是實際啟動並執行方法,找一下,注意注釋。先檢查channel是否能操作,如果不能操作,就走 default 邏輯。
loop: // pass 1 - look for something already waiting var dfl *scase var cas *scase for i := 0; i < int(sel.ncase); i++ { cas = &scases[pollorder[i]] c = cas.c switch cas.kind { // 接受資料 case caseRecv: sg = c.sendq.dequeue() // 如果有 sender 在等待 if sg != nil { goto recv } // 當前buffer中有資料 if c.qcount > 0 { goto bufrecv } // 關閉的channel if c.closed != 0 { goto rclose } case caseSend: if raceenabled { racereadpc(unsafe.Pointer(c), cas.pc, chansendpc) } // 關閉 if c.closed != 0 { goto sclose } // 有 receiver 正在等待 sg = c.recvq.dequeue() if sg != nil { goto send } // 有空間接受 if c.qcount < c.dataqsiz { goto bufsend } // 走default case caseDefault: dfl = cas } } if dfl != nil { selunlock(scases, lockorder) cas = dfl goto retc }