select
select เป็นโครงสร้างที่สามารถติดตามสถานะของหลาย channel พร้อมกัน ไวยากรณ์ของมันคล้ายกับ switch
import (
"context"
"log/slog"
"os"
"os/signal"
"time"
)
func main() {
finished := make(chan struct{})
ctx, stop := signal.NotifyContext(context.Background(), os.Kill, os.Interrupt)
defer stop()
slog.Info("running")
go func() {
time.Sleep(time.Second * 2)
finished <- struct{}{}
}()
select {
case <-ctx.Done():
slog.Info("shutting down")
case <-finished:
slog.Info("finished")
}
}โค้ดนี้ใช้การรวม context, channel, select ทั้งสามอย่างเข้าด้วยกันเพื่อใช้ตรรกะการออกจากโปรแกรมอย่างราบรื่นอย่างง่าย ในโค้ด select ติดตามทั้ง ctx.Done และ finished สอง channel พร้อมกัน เงื่อนไขการออกมีสองข้อ หนึ่งคือระบบปฏิบัติการส่งสัญญาณออก สองคือ finished channel มีข้อความให้อ่าน นั่นคืองานโค้ดผู้ใช้เสร็จสิ้น เช่นนี้เราสามารถทำงาน收尾เมื่อโปรแกรมออก
เป็นที่ทราบกันดีว่า select มีคุณสมบัติสำคัญสองข้อ หนึ่งคือไม่บล็อก ในโค้ดส่งและรับของ channel สามารถเห็นได้ว่ามีการจัดการสำหรับ select สามารถตัดสินว่า channel พร้อมใช้งานได้ในกรณีที่ไม่บล็อก สองคือการสุ่ม หากมีหลาย channel พร้อมใช้งาน มันจะสุ่มเลือกหนึ่งเพื่อทำงาน ไม่ปฏิบัติตามลำดับที่กำหนดไว้สามารถทำให้แต่ละ channel ได้รับการดำเนินการอย่างเท่าเทียมกัน มิฉะนั้นในกรณีสุดขั้วบาง channel อาจไม่ได้รับการประมวลผลเลย เนื่องจากการทำงานทั้งหมดเกี่ยวข้องกับ channel จึงแนะนำให้ อ่านบทความ chan ก่อน เมื่อเข้าใจ channel แล้วมาเข้าใจ select จะราบรื่นมาก
โครงสร้าง
ในขณะทำงานมีเพียงโครงสร้าง runtime.scase เท่านั้นที่แสดงถึง branch ของ select แต่ละ case ในขณะทำงานแสดงด้วย scase
type scase struct {
c *hchan // chan
elem unsafe.Pointer // data element
}ในนั้น c หมายถึง channel elem หมายถึงตัวชี้ขององค์ประกอบที่รับหรือส่ง ที่จริงแล้ว คำสั่ง select หมายถึงฟังก์ชัน runtime.selectgo
หลักการ
วิธีการใช้ select ถูกแบ่งโดย go เป็นสี่กรณีเพื่อการเพิ่มประสิทธิภาพ สามารถเห็นได้ในฟังก์ชัน cmd/compile/internal/walk.walkSelectCases สำหรับการจัดการตรรกะของสี่กรณีนี้
func walkSelectCases(cases []*ir.CommClause) []ir.Node {
ncas := len(cases)
sellineno := base.Pos
// optimization: zero-case select
if ncas == 0 {
return []ir.Node{mkcallstmt("block")}
}
// optimization: one-case select: single op.
if ncas == 1 {
...
}
// optimization: two-case select but one is default: single non-blocking op.
if ncas == 2 && dflt != nil {
...
}
...
return init
}การเพิ่มประสิทธิภาพ
คอมไพเลอร์จะเพิ่มประสิทธิภาพสำหรับสามกรณีแรก กรณีแรกคือจำนวน case เป็น 0 คือ select ว่าง เรารู้ว่าคำสั่ง select ว่างจะทำให้ goroutine ปัจจุบันบล็อกอย่างถาวร
select{}สาเหตุที่บล็อกเพราะคอมไพเลอร์แปลเป็นการเรียกฟังก์ชัน runtime.block โดยตรง
func block() {
gopark(nil, nil, waitReasonSelectNoCases, traceBlockForever, 1) // forever
}และฟังก์ชัน block เรียกฟังก์ชัน runtime.gopark ทำให้ goroutine ปัจจุบันเปลี่ยนเป็นสถานะ _Gwaiting และเข้าสู่การบล็อกถาวร ไม่ได้รับการจัดตารางเวลาอีกต่อไป
กรณีที่สอง มีเพียงหนึ่ง case และไม่ใช่ default กรณีนี้คอมไพเลอร์จะแปลเป็นการเรียกฟังก์ชันส่งรับ channel โดยตรง และยังเป็นแบบบล็อกด้วย ตัวอย่างเช่นโค้ดด้านล่าง
func main() {
ch := make(chan int)
select {
case <-ch:
// do something
}
}มันจะถูกแปลเป็นการเรียกฟังก์ชัน runtime.chanrecv1 โดยตรง สามารถเห็นได้จากโค้ดแอสเซมบลี
TEXT main.main(SB), ABIInternal, $2
...
LEAQ type:chan int(SB), AX
XORL BX, BX
PCDATA $1, $0
CALL runtime.makechan(SB)
XORL BX, BX
NOP
CALL runtime.chanrecv1(SB)
ADDQ $16, SP
POPQ BP
...ในกรณีที่มีเพียงหนึ่ง case การส่งข้อมูลไปยัง channel ก็เช่นเดียวกัน มันจะถูกแปลเป็นการเรียกฟังก์ชัน runtime.chansend1 โดยตรง เช่นเดียวกันเป็นแบบบล็อก
กรณีที่สาม มีสอง case และหนึ่งในนั้นคือ default
func main() {
ch := make(chan int)
select {
case ch <- 1:
// do something
default:
// do something
}
}กรณีนี้จะถูกแปลเป็นคำสั่ง if ที่เรียกฟังก์ชัน runtime.selectnbsend ดังนี้
if selectnbsend(ch, 1) {
// do something
} else {
// do something
}หากเป็นการรับข้อมูลจาก channel จะถูกแปลเป็นการเรียกฟังก์ชัน runtime.selectnbrecv
ch := make(chan int)
select {
case x, ok := <-ch:
// do something
default:
// do something
}if selected, ok = selectnbrecv(&v, c); selected {
// do something
} else {
// do something
}ควรสังเกตว่าในกรณีนี้การรับหรือส่ง channel เป็นแบบไม่บล็อก เราสามารถเห็นได้อย่างชัดเจนว่าพารามิเตอร์ block เป็น false
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
return chansend(c, elem, false, getcallerpc())
}
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected, received bool) {
return chanrecv(c, elem, false)
}และไม่ว่าจะเป็นการส่งหรือรับข้อมูลจาก channel เมื่อ block เป็น false จะมีเส้นทางด่วนที่สามารถตัดสินว่าสามารถส่งหรือรับได้โดยไม่ต้องล็อก ดังแสดงด้านล่าง
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
if !block && empty(c) {
if atomic.Load(&c.closed) == 0 {
return
}
return true, false
}
...
}
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if !block && c.closed == 0 && full(c) {
return false
}
...
}สำหรับการอ่าน channel หาก channel ว่างจะคืนค่าโดยตรง สำหรับการเขียน channel หาก channel ไม่ปิดและเต็มแล้วจะคืนค่าโดยตรง ในกรณีทั่วไปจะทำให้ goroutine บล็อก แต่เมื่อใช้ร่วมกับ select จะไม่บล็อก
การประมวลผล
สามกรณีข้างต้นเป็นเพียงการเพิ่มประสิทธิภาพสำหรับกรณีพิเศษ คำสั่ง select ที่ใช้ปกติจะถูกแปลเป็นการเรียกฟังก์ชัน runtime.selectgo ตรรกะการประมวลผลมีความยาวมากกว่า 400 บรรทัด
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool)คอมไพเลอร์จะรวบรวมทุกคำสั่ง case เป็นอาร์เรย์ scase แล้วส่งไปยังฟังก์ชัน selectgo หลังจากประมวลผลเสร็จแล้วจะคืนค่าสองค่า
- ค่าแรกคือดัชนี channel ที่สุ่มเลือก แสดงว่า channel ใดถูกประมวลผล หากไม่มีคืนค่า -1
- ค่าที่สองแสดงว่าการอ่าน channel สำเร็จหรือไม่
อธิบายพารามิเตอร์อย่างง่าย
cas0ตัวชี้หัวของอาร์เรย์scaseครึ่งแรกเก็บ write channel case ครึ่งหลังเก็บ read channel case แยกด้วยnsendsorder0ความยาวเป็นสองเท่าของอาร์เรย์scaseครึ่งแรกจัดสรรให้อาร์เรย์pollorderครึ่งหลังจัดสรรให้อาร์เรย์lockordernsendsและnrecvsแสดงจำนวน read/write channel case ผลรวมของทั้งสองคือจำนวน case ทั้งหมดblockแสดงว่าบล็อกหรือไม่ หากมีdefaultcase แสดงว่าไม่บล็อก ค่าเป็นfalseมิฉะนั้นเป็นtruepc0ชี้ไปยังหัวของอาร์เรย์[ncases]uintptrใช้สำหรับการวิเคราะห์ race ไม่มีส่วนช่วยในการเข้าใจ select
สมมติว่ามีโค้ดด้านล่าง
func main() {
ch := make(chan int)
select {
case ch <- 1:
println(1)
case ch <- 2:
println(2)
case ch <- 3:
println(3)
case ch <- 4:
println(4)
default:
println("default")
}
}ดูรูปแบบแอสเซมบลีของมัน เพื่อความสะดวกในการเข้าใจได้ละเว้นโค้ดบางส่วน
0x0000 00000 TEXT main.main(SB), ABIInterna
...
0x0023 00035 CALL runtime.makechan(SB)
0x0028 00040 MOVQ $1, main..autotmp_2+72(SP) // 1 2 3 4 ตัวแปรชั่วคราว
0x0031 00049 MOVQ $2, main..autotmp_4+64(SP)
0x003a 00058 MOVQ $3, main..autotmp_6+56(SP)
0x0043 00067 MOVQ $4, main..autotmp_8+48(SP)
...
0x00c8 00200 CALL runtime.selectgo(SB) // เรียกฟังก์ชัน runtime.selectgo
0x00cd 00205 TESTQ AX, AX
0x00d0 00208 JLT 352 // กระโดดไป branch default
0x00d6 00214 PCDATA $1, $-1
0x00d6 00214 JEQ 320 // กระโดดไป branch 4
0x00d8 00216 CMPQ AX, $1
0x00dc 00220 JEQ 288 // กระโดดไป branch 3
0x00de 00222 NOP
0x00e0 00224 CMPQ AX, $2
0x00e4 00228 JNE 258 // กระโดดไป branch 2
0x00e6 00230 PCDATA $1, $0
0x00e6 00230 CALL runtime.printlock(SB)
0x00eb 00235 MOVL $3, AX
0x00f0 00240 CALL runtime.printint(SB)
0x00f5 00245 CALL runtime.printnl(SB)
0x00fa 00250 CALL runtime.printunlock(SB)
0x00ff 00255 NOP
0x0100 00256 JMP 379
0x0102 00258 CALL runtime.printlock(SB)
0x0107 00263 MOVL $4, AX
0x010c 00268 CALL runtime.printint(SB)
0x0111 00273 CALL runtime.printnl(SB)
0x0116 00278 CALL runtime.printunlock(SB)
0x011b 00283 JMP 379
0x011d 00285 NOP
0x0120 00288 CALL runtime.printlock(SB)
0x0125 00293 MOVL $2, AX
0x012a 00298 CALL runtime.printint(SB)
0x012f 00303 CALL runtime.printnl(SB)
0x0134 00308 CALL runtime.printunlock(SB)
0x0139 00313 JMP 379
0x013b 00315 NOP
0x0140 00320 CALL runtime.printlock(SB)
0x0145 00325 MOVL $1, AX
0x014a 00330 CALL runtime.printint(SB)
0x014f 00335 CALL runtime.printnl(SB)
0x0154 00340 CALL runtime.printunlock(SB)
0x0159 00345 JMP 379
0x015b 00347 NOP
0x0160 00352 CALL runtime.printlock(SB)
0x0165 00357 LEAQ go:string."default\n"(SB)
0x016c 00364 MOVL $8, BX
0x0171 00369 CALL runtime.printstring(SB)
0x0176 00374 CALL runtime.printunlock(SB)
0x017b 00379 PCDATA $1, $-1
0x017b 00379 ADDQ $160, SP
0x0182 00386 POPQ BP
0x0183 00387 RETจะเห็นได้ว่าหลังจากเรียกฟังก์ชัน selectgo แล้วมีการตัดสิน+ตรรกะการกระโดดอยู่ ผ่านสิ่งเหล่านี้เรา不难反推รูปร่างเดิมของมัน
casei, ok := runtime.selectgo()
if casei == -1 {
println("default")
} else if casei == 3 {
println(4)
} else if casei == 2 {
println(3)
} else if casei == 1 {
println(2)
} else {
println(1)
}โค้ดที่คอมไพเลอร์สร้างอาจแตกต่างจากนี้เล็กน้อย แต่ความหมายโดยรวมน่าจะเหมือนกัน ดังนั้นหลังจากเรียกฟังก์ชัน selectgo คอมไพเลอร์จะใช้คำสั่ง if เพื่อตัดสินว่า channel ใดจะถูกดำเนินการ และก่อนเรียก คอมไพเลอร์จะสร้างลูป for เพื่อรวบรวมอาร์เรย์ scase แต่ที่นี่ละไว้
หลังจากทราบวิธีการใช้ฟังก์ชัน selectgo จากภายนอก ต่อไปมาเข้าใจว่าภายในฟังก์ชัน selectgo ทำงานอย่างไร อย่างแรกจะเริ่มต้นอาร์เรย์หลายอาร์เรย์ nsends+nrecvs แสดงจำนวน case ทั้งหมด จากโค้ดด้านล่างสามารถเห็นได้ว่าจำนวน case สูงสุดคือ 1 << 16 pollorder กำหนดลำดับการทำงานของ channel lockorder กำหนดลำดับการล็อกของ channel
cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
// ความยาวเป็นสองเท่าของอาร์เรย์ scase ครึ่งแรกจัดสรรให้อาร์เรย์ pollorder ครึ่งหลังจัดสรรให้อาร์เรย์ lockorder
order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))
ncases := nsends + nrecvs
scases := cas1[:ncases:ncases]
pollorder := order1[:ncases:ncases]
lockorder := order1[ncases:][:ncases:ncases]ต่อไปเริ่มต้นอาร์เรย์ pollorder มันเก็บดัชนีอาร์เรย์ scases ของ channel ที่รอการดำเนินการ
norder := 0
for i := range scases {
cas := &scases[i]
// Omit cases without channels from the poll and lock orders.
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]มันจะ遍历ทั้งอาร์เรย์ scases แล้วใช้ runtime.fastrandn สร้างตัวเลขสุ่มระหว่าง [0, i] แล้วสลับกับ i ในระหว่างกระบวนการจะข้าม case ที่ channel เป็น nil หลังจาก遍历เสร็จแล้วจะได้อาร์เรย์ pollorder ที่มีองค์ประกอบสลับแล้ว ดังแสดงด้านล่าง

แล้วใช้อาร์เรย์ pollorder เรียงลำดับตามขนาดที่อยู่ของ channel โดยใช้ heap sort จะได้อาร์เรย์ lockorder แล้วเรียก runtime.sellock เพื่อล็อกตามลำดับ
func sellock(scases []scase, lockorder []uint16) {
var c *hchan
for _, o := range lockorder {
c0 := scases[o].c
if c0 != c {
c = c0
lock(&c.lock)
}
}
}ควรสังเกตว่า การเรียงลำดับ channel ตามขนาดที่อยู่เพื่อหลีกเลี่ยง dead lock เพราะการดำเนินการ select เองไม่ต้องการล็อกอนุญาตให้ทำงานพร้อมกัน สมมติว่าล็อกตามลำดับสุ่มของ pollorder พิจารณาสถานการณ์ในโค้ดด้านล่าง
ch1 := make(chan int)
ch2 := make(chan int)
ch3 := make(chan int)
ch4 := make(chan int)
poll := func() {
select {
case ch1 <- 1:
println(1)
case ch2 <- 2:
println(2)
case ch3 <- 3:
println(3)
case ch4 <- 4:
println(4)
default:
println("default")
}
}
// A
go poll()
// B
go poll()
// C
go poll()สาม goroutine ABC ทั้งหมดมาถึงขั้นตอนการล็อกนี้ และลำดับการล็อกของพวกมันสุ่มไม่เหมือนกัน อาจทำให้เกิดสถานการณ์เช่นนี้ ดังแสดงด้านล่าง

สมมติว่าลำดับการล็อกของ ABC เหมือนกับภาพด้านบน โอกาสเกิด dead lock สูงมาก เช่น A จะถือล็อกของ ch2 ก่อน แล้วไปลองรับล็อกของ ch1 แต่สมมติว่า ch1 ถูก goroutine B ล็อกไว้แล้ว goroutine B จะไปลองรับล็อกของ ch2 เช่นนี้ก็เกิด dead lock แล้ว

หากทุก goroutine ล็อกตามลำดับเดียวกัน จะไม่เกิดปัญหา dead lock นี่คือเหตุผลพื้นฐานที่ lockorder ต้องเรียงลำดับตามขนาดที่อยู่
หลังจากล็อกแล้ว ก็เข้าสู่ขั้นตอนการประมวลผลจริง อย่างแรก遍历อาร์เรย์ pollorder ตามลำดับที่สลับก่อนหน้า访问 channel逐个遍历หา channel ที่พร้อมใช้งาน
for _, casei := range pollorder {
casi = int(casei)
cas = &scases[casi]
c = cas.c
if casi >= nsends { // read channel
sg = c.sendq.dequeue()
if sg != nil {
goto recv
}
if c.qcount > 0 {
goto bufrecv
}
if c.closed != 0 {
goto rclose
}
} else { // write channel
if c.closed != 0 {
goto sclose
}
sg = c.recvq.dequeue()
if sg != nil {
goto send
}
if c.qcount < c.dataqsiz {
goto bufsend
}
}
}จะเห็นได้ว่าที่นี่มีการจัดการ 6 กรณีสำหรับ read/write channel ต่อไปอธิบายแต่ละกรณี กรณีแรก อ่าน channel และมีผู้ส่งกำลังรอส่ง ที่นี่จะไปที่ฟังก์ชัน runtime.recv บทบาทของมันได้กล่าวไปแล้ว มันจะปลุก goroutine ผู้ส่งในที่สุด และก่อนปลุกฟังก์ชัน callback จะปลดล็อก channel ทั้งหมด
recv:
// can receive from sleeping sender (sg)
recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
recvOK = true
goto retcกรณีที่สอง อ่าน channel ไม่มีผู้ส่งกำลังรอ จำนวนองค์ประกอบในบัฟเฟอร์มากกว่า 0 ที่นี่จะอ่านข้อมูลจากบัฟเฟอร์โดยตรง ตรรกะของมันเหมือนกับใน runtime.chanrecv แล้วปลดล็อก
bufrecv:
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กรณีที่สาม อ่าน channel แต่ channel ปิดแล้ว และไม่มีองค์ประกอบเหลือในบัฟเฟอร์ ที่นี่จะปลดล็อกแล้วคืนค่าโดยตรง
rclose:
selunlock(scases, lockorder)
recvOK = false
if cas.elem != nil {
typedmemclr(c.elemtype, cas.elem)
}
goto retcกรณีที่สี่ ส่งข้อมูลไปยัง channel ที่ปิดแล้ว ที่นี่จะปลดล็อกแล้ว panic
sclose:
selunlock(scases, lockorder)
panic(plainError("send on closed channel"))กรณีที่ห้า มีผู้รับกำลังบล็อกอยู่ ที่นี่จะเรียกฟังก์ชัน runitme.send และในที่สุดปลุก goroutine ผู้รับ ก่อนปลุกฟังก์ชัน callback จะปลดล็อก channel ทั้งหมด
send:
send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
goto retcกรณีที่หก ไม่มี goroutine ผู้รับรอ 将要ส่งข้อมูล放入บัฟเฟอร์ แล้วปลดล็อก
bufsend:
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แล้วทุกกรณีข้างต้นสุดท้ายจะเข้าสู่ branch retc และสิ่งที่ต้องทำมีเพียงคืนค่าดัชนี channel ที่เลือก casi และ recvOk ที่แสดงว่าอ่านสำเร็จหรือไม่
retc:
return casi, recvOKกรณีที่เจ็ด ไม่พบ channel ที่พร้อมใช้งาน และโค้ดมี branch default จะปลดล็อก channel แล้วคืนค่าโดยตรง ที่นี่คืนค่า casi เป็น -1 แสดงว่าไม่มี channel ที่พร้อมใช้งาน
if !block {
selunlock(scases, lockorder)
casi = -1
goto retc
}กรณีสุดท้าย ไม่พบ channel ที่พร้อมใช้งาน และโค้ดไม่มี branch default ดังนั้น goroutine ปัจจุบันจะเข้าสู่สถานะบล็อก ก่อนนั้น selectgo จะเพิ่ม goroutine ปัจจุบันเข้าไปในคิว recvq/sendq ของ channel ที่ติดตามทั้งหมด
gp = getg()
nextp = &gp.waiting
for _, casei := range lockorder {
casi = int(casei)
cas = &scases[casi]
c = cas.c
sg := acquireSudog()
sg.g = gp
sg.isSelect = true
sg.elem = cas.elem
sg.releasetime = 0
sg.c = c
*nextp = sg
nextp = &sg.waitlink
if casi < nsends {
c.sendq.enqueue(sg)
} else {
c.recvq.enqueue(sg)
}
}ที่นี่จะสร้าง若干个 sudog และเชื่อมต่อกับ channel ที่ตรงกัน ดังแสดงด้านล่าง

แล้วบล็อกโดย runtime.gopark ก่อนบล็อกจะปลดล็อก channel งานปลดล็อกเสร็จโดยฟังก์ชัน runtime.selparkcommit มันถูกส่งเป็นฟังก์ชัน callback เข้าไปใน gopark
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.
gp.parkingOnChan.Store(true)
gopark(selparkcommit, nil, waitReasonSelect, traceBlockSelect, 1)
gp.activeStackChans = falseสิ่งแรกหลังจากถูกปลุกคือ解除การเชื่อมต่อ sudog กับ channel
sellock(scases, lockorder)
gp.selectDone.Store(0)
sg = (*sudog)(gp.param)
gp.param = nil
casi = -1
cas = nil
caseSuccess = false
sglist = gp.waiting
// Clear all elem before unlinking from gp.waiting.
for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink {
sg1.isSelect = false
sg1.elem = nil
sg1.c = nil
}
gp.waiting = nilแล้วลบ sudog ออกจากคิวรอของ channel ก่อนหน้า
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
}ในกระบวนการข้างต้นจะต้องหา channel ที่ goroutine ผู้ปลุกจัดการ แล้วทำการประมวลผลสุดท้ายตาม caseSuccess สำหรับการเขียน操作 sg.success เป็น false แสดงว่า channel ปิดแล้ว และทั้ง go runtime มีเพียงฟังก์ชัน close เท่านั้นที่จะตั้งค่าฟิลด์นี้เป็น false โดย主动 แสดงว่า goroutine ปัจจุบันถูกปลุกโดยผู้ปลุกผ่านฟังก์ชัน close สำหรับการอ่าน操作 หากถูกปลุกโดยผู้ส่ง การอ่านข้อมูลก็เสร็จโดยผู้ส่งผ่านฟังก์ชัน runtime.send ก่อนถูกปลุกแล้ว ค่าเป็น true หากถูกปลุกโดยฟังก์ชัน close เหมือนกับข้างต้นคือคืนค่าโดยตรง
c = cas.c
if casi < nsends {
if !caseSuccess {
goto sclose
}
} else {
recvOK = caseSuccess
}
selunlock(scases, lockorder)
goto retc到此整个 select 的逻辑都大致理清楚了,上面分了好几种情况,可见 select 处理起来还是比较复杂的。
