Go Select的實現

select語法總結 select對應的每個case如果有已經準備好的case 則進行chan讀寫操作;若沒有則執行defualt語句;若都沒有則阻塞當前goroutine,直到某個chan準備好可讀或可寫,完成對應的case後退出。

Select的內存布局

了解chanel的實現后對select的語法有個疑問,select如何實現多路復用的,為什麼沒有在第一個channel操作時阻塞 從而導致後面的case都執行不了。為了解決疑問,對應代碼看一下彙編調用了哪些runtime層的函數,發現select語法塊被編譯器翻譯成了以下過程。

創建select–>註冊case–>執行select–>釋放select

Advertisements

select { case c1 <-1: // non-blocking case <-c2: // non-blocking default: // will do this }
runtime.newselectruntime.selectsendruntime.selectrecvruntime.selectdefaultruntime.selectgo

select實際上是個hselect結構體,其中註冊的case放到scase中。scase保存有當前case操作的hchan。pollorder指向的是亂序后的scase序號。lockorder中將要保存的是每個case對應的hchan的地址。

Advertisements

type hselect struct { tcase uint16 // total count of scase[] ncase uint16 // currently filled scase[] pollorder *uint16 // case poll order lockorder **hchan // channel lock order scase [1]scase // one per case (in order of appearance)}type scase struct { elem unsafe.Pointer // data element c *hchan // chan pc uintptr // return pc kind uint16 so uint16 // vararg of selected bool receivedp *bool // pointer to received bool (recv2) releasetime int64}

select最後是[1]scase表示select中只保存了一個case的空間,說明select只是個頭部,select後面保存了所有的scase,這段Scases的大小就是tcase。在go runtime實現中經常看到這種頭部+連續內存的方式。

select的實現

select創建

在newSelect對象時已經知道了case的數目,並已經分配好上述空間。

func selectsize(size uintptr) uintptr { selsize := unsafe.Sizeof(hselect{}) + (size-1)*unsafe.Sizeof(hselect{}.scase[0]) + size*unsafe.Sizeof(*hselect{}.lockorder) + size*unsafe.Sizeof(*hselect{}.pollorder) return round(selsize, _Int64Align) }  func newselect(sel *hselect, selsize int64, size int32) { if selsize != int64(selectsize(uintptr(size))) { print("runtime: bad select size ", selsize, ", want ", selectsize(uintptr(size)), "\n") throw("bad select size") } sel.tcase = uint16(size) sel.ncase = 0 sel.lockorder = (**hchan)(add(unsafe.Pointer(&sel.scase), uintptr(size)*unsafe.Sizeof(hselect{}.scase[0]))) sel.pollorder = (*uint16)(add(unsafe.Pointer(sel.lockorder), uintptr(size)*unsafe.Sizeof(*hselect{}.lockorder))) }

註冊case

case channel有三種註冊 selectsend selectrecv selectdefault,分別對應著不同的case。他們的註冊方式一致,都是ncase+1,然後按照當前的index填充scases域的scase數組的相關欄位,主要是用case中的chan和case類型填充c和kind欄位。

func selectsendImpl(sel *hselect, c *hchan, pc uintptr, elem unsafe.Pointer, so uintptr) { i := sel.ncase sel.ncase = i + 1 cas := (*scase)(add(unsafe.Pointer(&sel.scase), uintptr(i)*unsafe.Sizeof(sel.scase[0]))) cas.pc = pc cas.c = c cas.so = uint16(so) cas.kind = caseSend cas.elem = elem}

select執行

pollorder保存的是scase的序號,亂序是為了之後執行時的隨機性。

lockorder保存了所有case中channel的地址,這裡按照地址大小堆排了一下lockorder對應的這片連續內存。對chan排序是為了去重,保證之後對所有channel上鎖時不會重複上鎖。

select語句執行時會對整個chanel加鎖

select語句會創建select對象 如果放在for循環中長期執行可能會頻繁的分配內存

select執行過程總結如下:

  • 通過pollorder的序號,遍歷scase找出已經準備好的case。如果有就執行普通的chan讀寫操作。其中準備好的case是指可以不阻塞完成讀寫chan的case,或者讀已經關閉的chan的case

  • 如果沒有準備好的case,則嘗試defualt case。

  • 如果以上都沒有,則把當前的G封裝好掛到scase所有chan的阻塞鏈表中,按照chan的操作類型掛到sendq或recvq中。

  • 這個G被某個chan喚醒,遍歷scase找到目標case,放棄當前G在其他chan中的等待,返回。

func selectgoImpl(sel *hselect) (uintptr, uint16) { // 對pollorder亂序 填充序號 // 對lockorder排序 填充scase中對應的hchan // 通過lockorder遍歷每個chan上鎖 sellock(sel)loop: // 按照pollorder的順序遍歷scase 查看有沒有case已經準備好 for i := 0; i < int(sel.ncase); i++ { cas = &scases[pollorder[i]] switch cas.kind { case caseRecv: case caseSend: case caseDefault: dfl = cas } } // 如果沒有準備好的scase 則嘗試執行defaut if dfl != nil { selunlock(sel) cas = dfl goto retc } // 如果沒有任何可以執行的case 將當前的G掛到所有case對應的chan // 的等待鏈表sendq或recvq上 等待被喚醒 for i := 0; i < int(sel.ncase); i++ { cas = &scases[pollorder[i]] c = cas.c sg := acquireSudog() switch cas.kind { case caseRecv: c.recvq.enqueue(sg) case caseSend: c.sendq.enqueue(sg) } } gp.param = nil gopark(selparkcommit, unsafe.Pointer(sel), "select", traceEvGoBlockSelect|futile, 2) // 被喚醒后又上鎖! sellock(sel) sg = (*sudog)(gp.param) gp.param = nil // 喚醒了當前G的sudoG是sg 遍歷之前保存的sglist鏈表匹配 for i := int(sel.ncase) - 1; i >= 0; i-- { k = &scases[pollorder[i]] if sg == sglist { cas = k } else { // 若不匹配則收回當前G在這個chan中的排隊 c = k.c if k.kind == caseSend { c.sendq.dequeueSudoG(sglist) } else { c.recvq.dequeueSudoG(sglist) } } sgnext = sglist.waitlink releaseSudog(sglist) sglist = sgnext } selunlock(sel) goto retcretc: return cas.pc, cas.so}

參考文章

select in go runtime

Go1.5源碼剖析

Advertisements

你可能會喜歡