go 中 select 原始碼閱讀

ZhanLi發表於2022-04-16

深入瞭解下 go 中的 select

前言

這裡藉助於幾個經常遇到的 select 的使用 demo 來作為開始,先來看看,下面幾個 demo 的輸出情況

1、栗子一

func main() {
	chan1 := make(chan int)
	chan2 := make(chan int)

	go func() {
		chan1 <- 1
	}()

	go func() {
		chan2 <- 1
	}()

	select {
	case <-chan1:
		fmt.Println("chan1 ready.")
	case <-chan2:
		fmt.Println("chan2 ready.")
	default:
		fmt.Println("default")
	}
}

select 中的 case 執行是隨機的,所以當 case 監聽的 channel 有資料傳入,就執行相應的流程並退出 select,如果對應的 case 沒有收到 channel 的資料,就執行 default 語句,然後退出 select。

上面的協程啟動時間是無法預估的,所以上面的兩個 case 和 default ,都有機會執行。

可能的輸出

可能輸出1、

chan1 ready.

可能輸出2、

chan2 ready.

可能輸出3、

default

2、栗子二

func main() {
	chan1 := make(chan int)
	chan2 := make(chan int)

	go func() {
		close(chan1)
	}()

	go func() {
		close(chan2)
	}()

	select {
	case <-chan1:
		fmt.Println("chan1 ready.")
	case <-chan2:
		fmt.Println("chan2 ready.")
	default:
		fmt.Println("default")
	}
}

已經關閉的 channel ,使用 select 是可以從中讀出對應的零值,同時兩面關閉 channel 的協程的執行實際也是不可控的,原則上,上面兩個 case 和 default 都有可能被執行。

可能的輸出

可能輸出1、

chan1 ready.

可能輸出2、

chan2 ready.

可能輸出3、

default

3、栗子三

func main() {
	select {}
}

上面這個,應為沒有機會退出,所以會發生死鎖

看下原始碼實現

select 中的多個 case 是隨機觸發執行的,一次只有一個 case 得到執行。如果我們按照順序依次判斷,那麼後面的條件永遠都會得不到執行,而隨機的引入就是為了避免飢餓問題的發生。

1、如果沒有 default 分支

如果沒有 default 分支,select 將會一直處於阻塞狀態,直到其中的一個 case 就緒;

2、如果有 default 分支

如果有 default 分支,隨機將 case 分支遍歷一遍,如果有 case 分支可執行,處理對應的 case 分支;

如果遍歷完 case 分支,沒有可執行的分支,執行 default 分支。

原始碼版本 go version go1.16.13 darwin/amd64

原始碼包 src/runtime/select.go 定義了表示case語句的資料結構:

type scase struct {
	c    *hchan         // chan
	elem unsafe.Pointer // data element
}

c為當前 case 語句所操作的 channel 指標,這也說明了一個 case 語句只能操作一個 channel。

編譯階段,select 對應的 opType 是 OSELECT,select 語句在編譯期間會被轉換成 OSELECT 節點。

// https://github.com/golang/go/blob/release-branch.go1.16/src/cmd/compile/internal/gc/syntax.go#L922
OSELECT // select { List } (List is list of OCASE)

如果是 OSELECT 就會呼叫 walkselect(),然後 walkselect() 最後呼叫 walkselectcases()

// https://github.com/golang/go/blob/release-branch.go1.16/src/cmd/compile/internal/gc/walk.go#L104
// The result of walkstmt MUST be assigned back to n, e.g.
// 	n.Left = walkstmt(n.Left)
func walkstmt(n *Node) *Node {
	if n == nil {
		return n
	}

	setlineno(n)

	walkstmtlist(n.Ninit.Slice())

	switch n.Op {
		...
	case OSELECT:
		walkselect(n)

	case OSWITCH:
		walkswitch(n)

	case ORANGE:
		n = walkrange(n)
	}

	if n.Op == ONAME {
		Fatalf("walkstmt ended up with name: %+v", n)
	}
	return n
}

// https://github.com/golang/go/blob/release-branch.go1.16/src/cmd/compile/internal/gc/select.go#L90
func walkselect(sel *Node) {
	lno := setlineno(sel)
	if sel.Nbody.Len() != 0 {
		Fatalf("double walkselect")
	}

	init := sel.Ninit.Slice()
	sel.Ninit.Set(nil)
    // 呼叫walkselectcases
	init = append(init, walkselectcases(&sel.List)...)
	sel.List.Set(nil)

	sel.Nbody.Set(init)
	walkstmtlist(sel.Nbody.Slice())

	lineno = lno
}

上面的呼叫邏輯,select 的邏輯是在 walkselectcases() 函式中完成的,這裡來重點看下

walkselectcases() 在處理中會分成下面幾種情況來處理

1、select 中不存在 case, 直接堵塞;

2、select 中僅存在一個 case;

3、select 中存在兩個 case,其中一個是 default;

4、其他 select 情況如: 包含多個 case 並且有 default 等。

// https://github.com/golang/go/blob/release-branch.go1.16/src/cmd/compile/internal/gc/select.go#L108
func walkselectcases(cases *Nodes) []*Node {
	// 獲取 case 分支的數量
	n := cases.Len()

	// 優化: 沒有 case 的情況
	if n == 0 {
		// 翻譯為:block()
		...
		return
	}

	// 優化: 只有一個 case 的情況
	if n == 1 {
		// 翻譯為:if ch == nil { block() }; n;
		...
		return
	}

	// 優化: select 中存在兩個 case,其中一個是 default 的情況
	if n == 2 {
		// 翻譯為:傳送或接收
		// if selectnbsend(c, v) { body } else { default body }

		// 接收 
		// if selectnbrecv(&v, &received, c) { body } else { default body }
		return
	}

	// 一般情況,呼叫 selecggo
	...
}

1、不存在 case

如果不存在 case ,空的 select 語句會直接阻塞當前 Goroutine,導致 Goroutine 進入無法被喚醒的永久休眠狀態。

// https://github.com/golang/go/blob/release-branch.go1.16/src/cmd/compile/internal/gc/select.go#L108
func walkselectcases(cases *Nodes) []*Node {
	n := cases.Len()

	if n == 0 {
		return []*Node{mkcall("block", nil, nil)}
	}
	...
}

// 呼叫 runtime.gopark 讓出當前 Goroutine 對處理器的使用權並傳入等待原因 waitReasonSelectNoCases。
func block() {
	gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1)
}

如果沒有 case,導致 Goroutine 進入無法被喚醒的永久休眠狀態,會觸發 deadlock!

2、select 中僅存在一個 case

如果只有一個 case ,編譯器會將 select 改寫成 if 條件語句。

// 改寫前
select {
case v, ok <-ch: // case ch <- v
    ...    
}

// 改寫後
if ch == nil {
    block()
}
v, ok := <-ch // case ch <- v
...

如果只有一個 case ,walkselectcases 會將 select 根據收發情況裝換成 if 語句,如果 case 中的 Channel 是空指標時,會直接掛起當前 Goroutine 並陷入永久休眠。

3、select 中存在兩個 case,其中一個是 default

傳送值

在 walkselectcases 中 OSEND,對應的就是向 channel 中傳送資料,如果是傳送的話,會翻譯成下面的語句

select {
case ch <- i:
    ...
default:
    ...
}

if selectnbsend(ch, i) {
    ...
} else {
    // default body
    ...
}

func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
	return chansend(c, elem, false, getcallerpc())
}

如果是傳送,這裡翻譯之後最終呼叫 chansend 向 channel 中傳送資料

// 這裡提供了一個 block,引數設定成 true,那麼表示當前傳送操作是阻塞的
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	...
	// 對於不阻塞的 send,快速檢測失敗場景
	//
	// 如果 channel 未關閉且 channel 沒有多餘的緩衝空間。這可能是:
	// 1. channel 是非緩衝型的,且等待接收佇列裡沒有 goroutine
	// 2. channel 是緩衝型的,但迴圈陣列已經裝滿了元素
	if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
		(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
		return false
	}
	...
}

總結下

1、如果 block 為 true 表示當前向 channel 中的資料傳送是阻塞的。這裡可以看到 selectnbsend 中傳入的是 false,說明 channel 的傳送不會阻塞 select。

2、對於不阻塞的傳送,會進行下面的檢測,如果 channel 未關閉且 channel 沒有多餘的緩衝空間,就會傳送失敗,然後跳出當前的 case,走到 default 的邏輯。

如果 channel 未關閉且 channel 沒有多餘的緩衝空間。這可能是:

  • 1、channel 是非緩衝型的,且等待接收佇列裡沒有 goroutine;

  • 2、channel 是緩衝型的,但迴圈陣列已經裝滿了元素;

接收值

在 walkselectcases 函式中可以看到,接收方式會有兩個,分別是 OSELRECV 和 OSELRECV2

// https://github.com/golang/go/blob/release-branch.go1.16/src/cmd/compile/internal/gc/walk.go#L104
func walkselectcases(cases *Nodes) []*Node {
	...
	// optimization: two-case select but one is default: single non-blocking op.
	if ncas == 2 && dflt != nil {
		switch n.Op {
		default:
			Fatalf("select %v", n.Op)

		case OSELRECV:
			// if selectnbrecv(&v, c) { body } else { default body }
			...
			r.Left = mkcall1(chanfn("selectnbrecv", 2, ch.Type), types.Types[TBOOL], &r.Ninit, elem, ch)

		case OSELRECV2:
			// if selectnbrecv2(&v, &received, c) { body } else { default body }
			...
			r.Left = mkcall1(chanfn("selectnbrecv2", 2, ch.Type), types.Types[TBOOL], &r.Ninit, elem, receivedp, ch)
		}

		r.Left = typecheck(r.Left, ctxExpr)
		r.Nbody.Set(cas.Nbody.Slice())
		r.Rlist.Set(append(dflt.Ninit.Slice(), dflt.Nbody.Slice()...))
		return []*Node{r, nod(OBREAK, nil, nil)}
	}
	...
}

walkselectcases 對這兩種情況的改寫

selectnbrecv

select {
case v = <-c:
	...
default:
	...
}

// 改寫後

if selectnbrecv(&v, c) {
	...
} else {
    // default body
	...
}

selectnbrecv2

select {
case v, ok = <-c:
	... foo
default:
	... bar
}

// 改寫後

if c != nil && selectnbrecv2(&v, &ok, c) {
	... foo
} else {
    // default body
	... bar
}

selectnbrecv 和 selectnbrecv2 有什麼區別呢?

// https://github.com/golang/go/blob/release-branch.go1.16/src/runtime/chan.go#L707
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
	selected, _ = chanrecv(c, elem, false)
	return
}

func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) {
	// TODO(khr): just return 2 values from this function, now that it is in Go.
	selected, *received = chanrecv(c, elem, false)
	return
}

可以發現只是針對返回的值處理不同,selectnbrecv2 多了一個是否 received 的 bool 值

總結下:

對於接收值的 case 會有兩種處理方式,這兩種,區別在於是否將 received 的 bool 值傳送給呼叫方

4、多個 case 的場景

多個 case 的場景

1、會將其中所有 case 轉化為 scase 結構體;

2、呼叫執行時函式 selectgo 選取觸發的 scase 結構體;

3、通過 for 迴圈生成一組 if 語句,來判斷是否選中 case;

這裡來看下 selectgo 的實現

這裡看下函式的幾個引數

cas0:為 scase 陣列的首地址,selectgo() 就是從這些 scase 中找出一個返回;

order0:為一個兩倍 cas0 陣列長度的 buffer,儲存 scase 隨機序列 pollorder 和 scase 中 channel 地址序列 lockorder,陣列前一半是 pollorder,後一半用來 lockorder;

  • pollorder:每次 selectgo 執行都會把 scase 序列打亂,以達到隨機檢測 case 的目的;

  • lockorder:所有 case 語句中 channel 序列,以達到去重防止對 channel 加鎖時重複加鎖的目的;

pc0:對於競態檢測器構建,pc0 指向一個陣列型別[ncases]uintptr (也在棧上);對於其他版本,它設定為 nil;

nsends: 傳送的 case 的個數;

nrecvs: 接收的 case 的個數;

block: 表示是否存在 default,沒有 default 就表示 select 是阻塞的。

看下返回的資料

int: 選中case的編號,這個case編號跟程式碼一致;

bool: 是否成功從channle中讀取了資料,如果選中的case是從channel中讀資料,則該返回值表示是否讀取成功。

具體的實現邏輯

1、打亂 scase 的順序,鎖定 scase 語句中所有的 channel;

2、按照隨機順序檢測 scase 中的 channel 是否ready;

  • 2.1 如果 case 可讀,則讀取 channel 中資料,解鎖所有的 channel,然後返回(case index, true)

  • 2.2 如果 case 可寫,則將資料寫入 channel,解鎖所有的channel,然後返回(case index, false)

  • 2.3 所有 case 都未 ready,並且有 default 語句,則解鎖所有的channel,然後返回 (default index, false)

3、所有 case 都未 ready,且沒有 default 語句

  • 3.1 將當前協程加入到所有 channel 的等待佇列

  • 3.2 當將協程轉入阻塞,等待被喚醒

4、喚醒後返回 channel 對應的case index

  • 4.1 如果是讀操作,解鎖所有的 channel,然後返回(case index, true)

  • 4.2 如果是寫操作,解鎖所有的 channel,然後返回(case index, false)

這裡來分析下 selectgo 的具體實現

1、打亂 case 的順序
// https://github.com/golang/go/blob/release-branch.go1.16/src/runtime/select.go#L121
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
	...
	// 生成隨機順序
	norder := 0
	for i := range scases {
		cas := &scases[i]

		// 忽略輪詢和鎖定命令中沒有通道的情況
		if cas.c == nil {
			cas.elem = nil // allow GC
			continue
		}

		j := fastrandn(uint32(norder + 1))
		pollorder[norder] = pollorder[j]
		pollorder[j] = uint16(i)
		norder++
	}
	pollorder = pollorder[:norder]
	lockorder = lockorder[:norder]

	// 根據 channel 地址進行排序,決定獲取鎖的順序
	for i := range lockorder {
		j := i
		// Start with the pollorder to permute cases on the same channel.
		c := scases[pollorder[i]].c
		for j > 0 && scases[lockorder[(j-1)/2]].c.sortkey() < c.sortkey() {
			k := (j - 1) / 2
			lockorder[j] = lockorder[k]
			j = k
		}
		lockorder[j] = pollorder[i]
	}
	...

	// 鎖定選中的 channel
	sellock(scases, lockorder)
	...
}

select 中的多個 case 是隨機觸發執行的,一次只有一個 case 得到執行。如果我們按照順序依次判斷,那麼後面的條件永遠都會得不到執行,而隨機的引入就是為了避免飢餓問題的發生。

所以可以看到上面會將 scase 序列打亂,以達到隨機檢測 case 的目的,然後記錄到 pollorder 中。

2、找出已經 ready 的 case
// https://github.com/golang/go/blob/release-branch.go1.16/src/runtime/select.go#L121
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
	...
	var (
		gp     *g
		sg     *sudog
		c      *hchan
		k      *scase
		sglist *sudog
		sgnext *sudog
		qp     unsafe.Pointer
		nextp  **sudog
	)

	// pass 1 - 遍歷所有 scase,確定已經準備好的 scase
	var casi int
	var cas *scase
	var caseSuccess bool
	var caseReleaseTime int64 = -1
	var recvOK bool
	// 因為上面已經將scases隨機寫入到pollorder中
	// 所以這裡的遍歷相比於原 cas0的順序,就是隨機的
	for _, casei := range pollorder {
		casi = int(casei)
		cas = &scases[casi]
		c = cas.c
		// 接收資料
		if casi >= nsends {
			// 有 goroutine 等待傳送資料
			sg = c.sendq.dequeue()
			if sg != nil {
				goto recv
			}
			// 緩衝區有資料
			if c.qcount > 0 {
				goto bufrecv
			}
			// 通道關閉
			if c.closed != 0 {
				goto rclose
			}
			// 傳送資料
		} else {
			if raceenabled {
				racereadpc(c.raceaddr(), casePC(casi), chansendpc)
			}
			// 判斷通道的關閉情況
			if c.closed != 0 {
				goto sclose
			}
			// 接收等待佇列有 goroutine
			sg = c.recvq.dequeue()
			if sg != nil {
				goto send
			}
			// 緩衝區有空位置
			if c.qcount < c.dataqsiz {
				goto bufsend
			}
		}
	}

	// 如果不阻塞,意味著有 default,準備退出select
	if !block {
		selunlock(scases, lockorder)
		casi = -1
		goto retc
	}

	...

bufrecv:
	// 可以從 buffer 接收 
	if raceenabled {
		if cas.elem != nil {
			raceWriteObjectPC(c.elemtype, cas.elem, casePC(casi), chanrecvpc)
		}
		racenotify(c, c.recvx, nil)
	}
	if msanenabled && cas.elem != nil {
		msanwrite(cas.elem, c.elemtype.size)
	}
	recvOK = true
	qp = chanbuf(c, c.recvx)
	if cas.elem != nil {
		typedmemmove(c.elemtype, cas.elem, qp)
	}
	typedmemclr(c.elemtype, qp)
	c.recvx++
	if c.recvx == c.dataqsiz {
		c.recvx = 0
	}
	c.qcount--
	selunlock(scases, lockorder)
	goto retc

bufsend:
	// 可以傳送到 buffer
	if raceenabled {
		racenotify(c, c.sendx, nil)
		raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)
	}
	if msanenabled {
		msanread(cas.elem, c.elemtype.size)
	}
	typedmemmove(c.elemtype, chanbuf(c, c.sendx), cas.elem)
	c.sendx++
	if c.sendx == c.dataqsiz {
		c.sendx = 0
	}
	c.qcount++
	selunlock(scases, lockorder)
	goto retc

recv:
	// 可以從一個休眠的傳送方 (sg)直接接收
	recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
	if debugSelect {
		print("syncrecv: cas0=", cas0, " c=", c, "\n")
	}
	recvOK = true
	goto retc

rclose:
	// 在已經關閉的 channel 末尾進行讀
	selunlock(scases, lockorder)
	recvOK = false
	if cas.elem != nil {
		typedmemclr(c.elemtype, cas.elem)
	}
	if raceenabled {
		raceacquire(c.raceaddr())
	}
	goto retc

send:
	// 可以向一個休眠的接收方 (sg) 傳送
	if raceenabled {
		raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)
	}
	if msanenabled {
		msanread(cas.elem, c.elemtype.size)
	}
	send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
	if debugSelect {
		print("syncsend: cas0=", cas0, " c=", c, "\n")
	}
	goto retc

retc:
	if caseReleaseTime > 0 {
		blockevent(caseReleaseTime-t0, 1)
	}
	return casi, recvOK

sclose:
	// 向已關閉的 channel 進行傳送
	selunlock(scases, lockorder)
	panic(plainError("send on closed channel"))
}

1、因為上面已經將 scases 隨機寫入到 pollorder 中,所以這裡的遍歷相比於原 cas0 的順序,就是隨機的;

2、case 監聽的 channel 有兩種操作,讀取或者寫入;

讀取資料

  • 1、如果有傳送的 goroutine 在等待資料的接收,那麼直接從這個 goroutine 中讀出資料,結束 select;

  • 2、如果 channel 的緩衝區有資料,在緩衝去讀出資料, 結束 select;

  • 3、如果 channel 關閉了,讀出零值,結束 select。

所以可看出,已經關閉的 channel ,用 select 是可以讀出資料的。

傳送資料

  • 1、如果 channel 關閉了,這時候會觸發 panic,因為已經關閉的 channel 是不能傳送資料的;

  • 2、如果 channel 的接收等待佇列有 goroutine,說明有 goroutine ,正在阻塞等待從該 channel 中接收資料,那麼資料直接傳送給該 goroutine,結束 select;

  • 3、如果 channel 的緩衝區有資料,傳送到資料到 channel 的緩衝區中,結束 select。

如果傳送的 channel 中沒有快取空間,接收 channel 的快取空間為空。這時候該 select 將會阻塞。

如果有 block 為 false ,就表示 select 中有 default,然後執行 default 結束 select。

如果 block 為 true 表示沒有 default,需要在阻塞 select,細節見下文。

3、case 都沒 ready,且沒有 default
// https://github.com/golang/go/blob/release-branch.go1.16/src/runtime/select.go#L121
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
	...
	// pass 2 - 所有 channel 入隊,等待處理
	gp = getg()
	if gp.waiting != nil {
		throw("gp.waiting != nil")
	}
	nextp = &gp.waiting
	for _, casei := range lockorder {
		casi = int(casei)
		// 獲取一個 scase
		cas = &scases[casi]
		// 監聽的 channel
		c = cas.c
		// 構建sudog,設定這一次阻塞傳送的相關資訊
		sg := acquireSudog()
		sg.g = gp
		sg.isSelect = true
		// No stack splits between assigning elem and enqueuing
		// sg on gp.waiting where copystack can find it.
		sg.elem = cas.elem
		sg.releasetime = 0
		if t0 != 0 {
			sg.releasetime = -1
		}
		sg.c = c
		// 按鎖定順序構造等待列表。
		*nextp = sg
		nextp = &sg.waitlink

		if casi < nsends {
			c.sendq.enqueue(sg)
		} else {
			c.recvq.enqueue(sg)
		}
	}

	// goroutine 陷入睡眠,等待某一個 channel 喚醒 goroutine
	gp.param = nil
	// Signal to anyone trying to shrink our stack that we're about
	// to park on a channel. The window between when this G's status
	// changes and when we set gp.activeStackChans is not safe for
	// stack shrinking.
	atomic.Store8(&gp.parkingOnChan, 1)
	// 將當前的 Goroutine 陷入沉睡等待喚醒
	gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)
	gp.activeStackChans = false

	sellock(scases, lockorder)

	gp.selectDone = 0
	sg = (*sudog)(gp.param)
	gp.param = nil
	...
}

如果 case 都沒有 ready ,並沒有 default

這時候會迴圈構建 sudog 的佇列,並且按鎖定順序構造等待列表,附在 goroutine 中,然後使用 gopark 掛起當前 goroutine 等待排程器的喚醒。

4、喚醒後返回 channel 對應的 case
// https://github.com/golang/go/blob/release-branch.go1.16/src/runtime/select.go#L121
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
	...
	casi = -1
	cas = nil
	caseSuccess = false
	sglist = gp.waiting
	// 在從 gp.waiting 取消連結之前清除所有元素。
	for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink {
		sg1.isSelect = false
		sg1.elem = nil
		sg1.c = nil
	}
	gp.waiting = nil

	for _, casei := range lockorder {
		k = &scases[casei]
		if sg == sglist {
			// sg has already been dequeued by the G that woke us up.
			casi = int(casei)
			cas = k
			caseSuccess = sglist.success
			if sglist.releasetime > 0 {
				caseReleaseTime = sglist.releasetime
			}
		} else {
			c = k.c
			if int(casei) < nsends {
				c.sendq.dequeueSudoG(sglist)
			} else {
				c.recvq.dequeueSudoG(sglist)
			}
		}
		sgnext = sglist.waitlink
		sglist.waitlink = nil
		releaseSudog(sglist)
		sglist = sgnext
	}

	...

	selunlock(scases, lockorder)
	goto retc

	...
retc:
	if caseReleaseTime > 0 {
		blockevent(caseReleaseTime-t0, 1)
	}
	return casi, recvOK
	...
}

遍歷全部 case 時,先獲取當前 Goroutine 接收到的引數 sudog 結構,然後依次對比所有 case 對應的 sudog 結構找到被喚醒的 case,獲取該 case 對應的索引並返回。

因為已經找到了一個可執行的 case,剩下的 case 中沒有被用到的 sudog 就會被忽略並且釋放掉。為了不影響 Channel 的正常使用,我們還是需要將這些廢棄的 sudog 從 Channel 中出隊。

總結

1、空的 select 會發生死鎖;

2、select 中的 case 分支的執行是隨機的;

3、如果沒有 default 分支

如果沒有 default 分支,select 將會一直處於阻塞狀態,直到其中的一個 case 就緒;

4、如果有 default 分支

如果有 default 分支,隨機將 case 分支遍歷一遍,如果有 case 分支可執行,處理對應的 case 分支;

如果遍歷完 case 分支,沒有可執行的分支,執行 default 分支。

5、select 中向 channel 的傳送不會阻塞 select;

6、select 語句中讀操作要判斷是否成功讀取,關閉的 channel 也可以讀取。

參考

【Select 語句的本質】https://golang.design/under-the-hood/zh-cn/part1basic/ch03lang/chan/#select-
【select】https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-select/#52-select
【go原始碼閱讀之Select】https://nercoeus.github.io/2020/01/13/go原始碼閱讀之Select/
【GO專家程式設計】https://book.douban.com/subject/35144587/
【深入瞭解下 go 中的 select】https://github.com/boilingfrog/Go-POINT/blob/master/golang/select/select原始碼閱讀.md
【select原始碼閱讀】https://boilingfrog.github.io/2022/04/16/go中select原始碼閱讀/

相關文章