在Go中,要理解channel,首先需要認(rèn)識goroutine。
一、為什么會有g(shù)oroutine
現(xiàn)代操作系統(tǒng)中為我們提供了三種基本的構(gòu)造并發(fā)程序的方法:多進(jìn)程、I/O多路復(fù)用和多線程。其中最簡單的構(gòu)造方式當(dāng)屬多進(jìn)程,但是多進(jìn)程的并發(fā)程序,由于對進(jìn)程控制和進(jìn)程間通信開銷巨大,這樣的并發(fā)方式往往會很慢。
因此,操作系統(tǒng)提供了更小粒度的運(yùn)行單元:線程(確切叫法是內(nèi)核線程)。它是一種運(yùn)行在進(jìn)程上下文中的邏輯流,線程之間通過操作系統(tǒng)來調(diào)度,其調(diào)度模型如下圖所示。

多線程的并發(fā)方式,相較于多進(jìn)程而言要快得多。但是由于線程上下文切換總是不可避免的陷入內(nèi)核態(tài),它的開銷依然較大。那么有沒有不必陷入內(nèi)核態(tài)的運(yùn)行載體呢?有,用戶級線程。 用戶級線程的切換由用戶程序自己控制,不需要內(nèi)核干涉,因此少了進(jìn)出內(nèi)核態(tài)的消耗。

這里的用戶級線程就是協(xié)程(coroutine),它們的切換由運(yùn)行時系統(tǒng)來統(tǒng)一調(diào)度管理,內(nèi)核并不知道它的存在。協(xié)程是抽象于內(nèi)核線程之上的對象,一個內(nèi)核線程可以對應(yīng)多個協(xié)程。但最終的系統(tǒng)調(diào)用仍然需要內(nèi)核線程來完成。注意,線程的調(diào)度是操作系統(tǒng)來管理,是一種搶占式調(diào)度。而協(xié)程不同,協(xié)程之間需要合作,會主動交出執(zhí)行權(quán),是一種協(xié)作式調(diào)度,這也是為何被稱為協(xié)程的原因。
Go天生在語言層面支持了協(xié)程,即我們常說的goroutine。Go的runtime系統(tǒng)實(shí)現(xiàn)的是一種M:N調(diào)度模型,通過GMP對象來描述,其中G代表的就是協(xié)程,M是線程,P是調(diào)度上下文。在Go程序中,一個goroutine就代表著一個最小用戶代碼執(zhí)行流,它們也是并發(fā)流的最小單元。
二、channel的存在定位
從內(nèi)存的角度而言,并發(fā)模型只分兩種:基于共享內(nèi)存和基于消息通信(內(nèi)存拷貝)。在Go中,兩種并發(fā)模型的同步原語均有提供:sync.*和atomic.*代表的就是基于共享內(nèi)存;channel代表的就是基于消息通信。而Go提倡后者,它包括三大元素:goroutine(執(zhí)行體),channel(通信),select(協(xié)調(diào))。
Do not communicate by sharing memory; instead, share memory by communicating.
在Go中通過goroutine+channel的方式,可以簡單、高效地解決并發(fā)問題,channel就是goroutine之間的數(shù)據(jù)橋梁。
Concurrency is the key to designing high performance network services. Go's concurrency primitives (goroutines and channels) provide a simple and efficient means of expressing concurrent execution.
以下是一個簡單的channel使用示例代碼。
func goroutineA(ch <-chan int) {
fmt.Println("[goroutineA] want a data")
val := <- ch
fmt.Println("[goroutineA] received the data", val)
}
func goroutineB(ch chan<- int) {
time.Sleep(time.Second*1)
ch <- 1
fmt.Println("[goroutineB] send the data 1")
}
func main() {
ch := make(chan int, 1)
go goroutineA(ch)
go goroutineB(ch)
time.Sleep(2*time.Second)
}
上述過程趣解圖如下




三、channel源碼解析
channel源碼位于src/go/runtime/chan.go。本章內(nèi)容分為兩部分:channel內(nèi)部結(jié)構(gòu)和channel操作。
3.1 channel內(nèi)部結(jié)構(gòu)
ch := make(chan int,2)
對于以上channel的申明語句,我們可以在程序中加入斷點(diǎn),得到ch的信息如下。

很好,看起來非常的清晰。但是,這些信息代表的是什么含義呢?接下來,我們先看幾個重要的結(jié)構(gòu)體。
- hchan
當(dāng)我們通過make(chan Type, size)生成channel時,在runtime系統(tǒng)中,生成的是一個hchan結(jié)構(gòu)體對象。源碼位于src/runtime/chan.go
type hchan struct {
qcount uint // 循環(huán)隊(duì)列中數(shù)據(jù)數(shù)
dataqsiz uint // 循環(huán)隊(duì)列的大小
buf unsafe.Pointer // 指向大小為dataqsize的包含數(shù)據(jù)元素的數(shù)組指針
elemsize uint16 // 數(shù)據(jù)元素的大小
closed uint32 // 代表channel是否關(guān)閉
elemtype *_type // _type代表Go的類型系統(tǒng),elemtype代表channel中的元素類型
sendx uint // 發(fā)送索引號,初始值為0
recvx uint // 接收索引號,初始值為0
recvq waitq // 接收等待隊(duì)列,存儲試圖從channel接收數(shù)據(jù)(<-ch)的阻塞goroutines
sendq waitq // 發(fā)送等待隊(duì)列,存儲試圖發(fā)送數(shù)據(jù)(ch<-)到channel的阻塞goroutines
lock mutex // 加鎖能保護(hù)hchan的所有字段,包括waitq中sudoq對象
}
- waitq
waitq用于表達(dá)處于阻塞狀態(tài)的goroutines鏈表信息,first指向鏈頭goroutine,last指向鏈尾goroutine
type waitq struct {
first *sudog
last *sudog
}
- sudug
sudog代表的就是一個處于等待列表中的goroutine對象,源碼位于src/runtime/runtime2.go
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer // data element (may point to stack)
c *hchan // channel
...
}
為了更好理解hchan結(jié)構(gòu)體,我們將通過以下代碼來理解hchan中的字段含義。
package main
import "time"
func goroutineA(ch chan int) {
ch <- 100
}
func goroutineB(ch chan int) {
ch <- 200
}
func goroutineC(ch chan int) {
ch <- 300
}
func goroutineD(ch chan int) {
ch <- 300
}
func main() {
ch := make(chan int, 4)
for i := 0; i < 4; i++ {
ch <- i * 10
}
go goroutineA(ch)
go goroutineB(ch)
go goroutineC(ch)
go goroutineD(ch)
// 第一個sleep是為了給上足夠的時間讓所有g(shù)oroutine都已啟動
time.Sleep(time.Millisecond * 500)
time.Sleep(time.Second)
}
打開代碼調(diào)試功能,將程序運(yùn)行至斷點(diǎn)time.Sleep(time.Second)處,此時得到的chan信息如下。

在該channel中,通過make(chan int, 4)定義的channel大小為4,即dataqsiz的值為4。同時由于循環(huán)隊(duì)列中已經(jīng)添加了4個元素,所以qcount值也為4。此時,有4個goroutine(A-D)想發(fā)送數(shù)據(jù)給channel,但是由于存放數(shù)據(jù)的循環(huán)隊(duì)列已滿,所以只能進(jìn)入發(fā)送等待列表,即sendq。同時要注意到,此時的發(fā)送和接收索引值均為0,即下一次接收數(shù)據(jù)的goroutine會從循環(huán)隊(duì)列的第一個元素拿,發(fā)送數(shù)據(jù)的goroutine會發(fā)送到循環(huán)隊(duì)列的第一個位置。
上述hchan結(jié)構(gòu)可視化圖解如下

3.2 channel操作
將channel操作分為四部分:創(chuàng)建、發(fā)送、接收和關(guān)閉。
創(chuàng)建
本文的參考Go版本為1.15.2。其channel的創(chuàng)建實(shí)現(xiàn)代碼位于src/go/runtime/chan.go的makechan方法。
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// 發(fā)送元素大小限制
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
// 對齊檢查
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
// 判斷是否會內(nèi)存溢出
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
// 為構(gòu)造的hchan對象分配內(nèi)存
var c *hchan
switch {
// 無緩沖的channel或者元素大小為0的情況
case mem == 0:
c = (*hchan)(mallocgc(hchanSize, nil, true))
c.buf = c.raceaddr()
// 元素不包含指針的情況
case elem.ptrdata == 0:
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
// 元素包含指針
default:
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
// 初始化相關(guān)參數(shù)
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)
if debugChan {
print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
}
return c
}
可以看到,makechan方法主要就是檢查傳送元素的合法性,并為hchan分配內(nèi)存,初始化相關(guān)參數(shù),包括對鎖的初始化。
發(fā)送
channel的發(fā)送實(shí)現(xiàn)代碼位于src/go/runtime/chan.go的chansend方法。發(fā)送過程,存在以下幾種情況。
- 當(dāng)發(fā)送的channel為nil
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
往一個nil的channel中發(fā)送數(shù)據(jù)時,調(diào)用gopark函數(shù)將當(dāng)前執(zhí)行的goroutine從running態(tài)轉(zhuǎn)入waiting態(tài)。
- 往已關(guān)閉的channel中發(fā)送數(shù)據(jù)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
如果向已關(guān)閉的channel中發(fā)送數(shù)據(jù),會引發(fā)panic。
- 如果已經(jīng)有阻塞的接收goroutines(即recvq中指向非空),那么數(shù)據(jù)將被直接發(fā)送給接收goroutine。
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) }, 3)
return true
}
該邏輯的實(shí)現(xiàn)代碼在send方法和sendDirect中。
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
... // 省略了競態(tài)代碼
if sg.elem != nil {
sendDirect(c.elemtype, sg, ep)
sg.elem = nil
}
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
goready(gp, skip+1)
}
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
dst := sg.elem
typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
memmove(dst, src, t.size)
}
其中,memmove我們已經(jīng)在源碼系列中遇到多次了,它的目的是將內(nèi)存中src的內(nèi)容拷貝至dst中去。另外,注意到goready(gp, skip+1)這句代碼,它會使得之前在接收等待隊(duì)列中的第一個goroutine的狀態(tài)變?yōu)閞unnable,這樣go的調(diào)度器就可以重新讓該goroutine得到執(zhí)行。
- 對于有緩沖的channel來說,如果當(dāng)前緩沖區(qū)hchan.buf有可用空間,那么會將數(shù)據(jù)拷貝至緩沖區(qū)
if c.qcount < c.dataqsiz {
qp := chanbuf(c, c.sendx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
}
typedmemmove(c.elemtype, qp, ep)
// 發(fā)送索引號+1
c.sendx++
// 因?yàn)榇鎯?shù)據(jù)元素的結(jié)構(gòu)是循環(huán)隊(duì)列,所以當(dāng)當(dāng)前索引號已經(jīng)到隊(duì)末時,將索引號調(diào)整到隊(duì)頭
if c.sendx == c.dataqsiz {
c.sendx = 0
}
// 當(dāng)前循環(huán)隊(duì)列中存儲元素?cái)?shù)+1
c.qcount++
unlock(&c.lock)
return true
}
其中,chanbuf(c, c.sendx)是獲取指向?qū)?yīng)內(nèi)存區(qū)域的指針。typememmove會調(diào)用memmove方法,完成數(shù)據(jù)的拷貝工作。另外注意到,當(dāng)對hchan進(jìn)行實(shí)際操作時,是需要調(diào)用lock(&c.lock)加鎖,因此,在完成數(shù)據(jù)拷貝后,通過unlock(&c.lock)將鎖釋放。
- 有緩沖的channel,當(dāng)hchan.buf已滿;或者無緩沖的channel,當(dāng)前沒有接收的goroutine
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
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
通過getg獲取當(dāng)前執(zhí)行的goroutine。acquireSudog是先獲得當(dāng)前執(zhí)行g(shù)oroutine的線程M,再獲取M對應(yīng)的P,最后將P的sudugo緩存隊(duì)列中的隊(duì)頭sudog取出(詳見源碼src/runtime/proc.go)。通過c.sendq.enqueue將sudug加入到channel的發(fā)送等待列表中,并調(diào)用gopark將當(dāng)前goroutine轉(zhuǎn)為waiting態(tài)。
- 發(fā)送操作會對hchan加鎖。
- 當(dāng)recvq中存在等待接收的goroutine時,數(shù)據(jù)元素將會被直接拷貝給接收goroutine。
- 當(dāng)recvq等待隊(duì)列為空時,會判斷hchan.buf是否可用。如果可用,則會將發(fā)送的數(shù)據(jù)拷貝至hchan.buf中。
- 如果hchan.buf已滿,那么將當(dāng)前發(fā)送goroutine置于sendq中排隊(duì),并在運(yùn)行時中掛起。
- 向已經(jīng)關(guān)閉的channel發(fā)送數(shù)據(jù),會引發(fā)panic。
對于無緩沖的channel來說,它天然就是hchan.buf已滿的情況,因?yàn)樗膆chan.buf的容量為0。
package main
import "time"
func main() {
ch := make(chan int)
go func(ch chan int) {
ch <- 100
}(ch)
time.Sleep(time.Millisecond * 500)
time.Sleep(time.Second)
}
在上述示例中,發(fā)送goroutine向無緩沖的channel發(fā)送數(shù)據(jù),但是沒有接收goroutine。將斷點(diǎn)置于time.Sleep(time.Second),得到此時ch結(jié)構(gòu)如下。

可以看到,在無緩沖的channel中,其hchan的buf長度為0,當(dāng)沒有接收groutine時,發(fā)送的goroutine將被置于sendq的發(fā)送隊(duì)列中。
接收
channel的接收實(shí)現(xiàn)分兩種,v :=<-ch對應(yīng)于chanrecv1,v, ok := <- ch對應(yīng)于chanrecv2,但它們都依賴于位于src/go/runtime/chan.go的chanrecv方法。
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c, elem, true)
return
}
chanrecv的詳細(xì)代碼此處就不再展示,和chansend邏輯對應(yīng),具體處理準(zhǔn)則如下。
- 接收操作會對hchan加鎖。
- 當(dāng)sendq中存在等待發(fā)送的goroutine時,意味著此時的hchan.buf已滿(無緩存的天然已滿),分兩種情況(見代碼src/go/runtime/chan.go的recv方法):1. 如果是有緩存的hchan,那么先將緩沖區(qū)的數(shù)據(jù)拷貝給接收goroutine,再將sendq的隊(duì)頭sudog出隊(duì),將出隊(duì)的sudog上的元素拷貝至hchan的緩存區(qū)。 2. 如果是無緩存的hchan,那么直接將出隊(duì)的sudog上的元素拷貝給接收goroutine。兩種情況的最后都會喚醒出隊(duì)的sudog上的發(fā)送goroutine。
- 當(dāng)sendq發(fā)送隊(duì)列為空時,會判斷hchan.buf是否可用。如果可用,則會將hchan.buf的數(shù)據(jù)拷貝給接收goroutine。
- 如果hchan.buf不可用,那么將當(dāng)前接收goroutine置于recvq中排隊(duì),并在運(yùn)行時中掛起。
- 與發(fā)送不同的是,當(dāng)channel關(guān)閉時,goroutine還能從channel中獲取數(shù)據(jù)。如果recvq等待列表中有g(shù)oroutines,那么它們都會被喚醒接收數(shù)據(jù)。如果hchan.buf中還有未接收的數(shù)據(jù),那么goroutine會接收緩沖區(qū)中的數(shù)據(jù),否則goroutine會獲取到元素的零值。
以下是channel關(guān)閉之后,接收goroutine的讀取示例代碼。
func main() {
ch := make(chan int, 1)
ch <- 10
close(ch)
a, ok := <-ch
fmt.Println(a, ok)
b, ok := <-ch
fmt.Println(b, ok)
c := <-ch
fmt.Println(c)
}
//輸出如下
10 true
0 false
0
注意:在channel中進(jìn)行的所有元素轉(zhuǎn)移都伴隨著內(nèi)存的拷貝。
func main() {
type Instance struct {
ID int
name string
}
var ins = Instance{ID: 1, name: "Golang"}
ch := make(chan Instance, 3)
ch <- ins
fmt.Println("ins的原始值:", ins)
ins.name = "Python"
go func(ch chan Instance) {
fmt.Println("channel接收值:", <-ch)
}(ch)
time.Sleep(time.Second)
fmt.Println("ins的最終值:", ins)
}
// 輸出結(jié)果
ins的原始值: {1 Golang}
channel接收值: {1 Golang}
ins的最終值: {1 Python}
前半段圖解如下

后半段圖解如下

注意,如果把channel傳遞類型替換為Instance指針時,那么盡管channel存入到buf中的元素已經(jīng)是拷貝對象了,從channel中取出又被拷貝了一次。但是由于它們的類型是Instance指針,拷貝對象與原始對象均會指向同一個內(nèi)存地址,修改原有元素對象的數(shù)據(jù)時,會影響到取出數(shù)據(jù)。
func main() {
type Instance struct {
ID int
name string
}
var ins = &Instance{ID: 1, name: "Golang"}
ch := make(chan *Instance, 3)
ch <- ins
fmt.Println("ins的原始值:", ins)
ins.name = "Python"
go func(ch chan *Instance) {
fmt.Println("channel接收值:", <-ch)
}(ch)
time.Sleep(time.Second)
fmt.Println("ins的最終值:", ins)
}
// 輸出結(jié)果
ins的原始值: &{1 Golang}
channel接收值: &{1 Python}
ins的最終值: &{1 Python}
因此,在使用channel時,盡量避免傳遞指針,如果傳遞指針,則需謹(jǐn)慎。
關(guān)閉
channel的關(guān)閉實(shí)現(xiàn)代碼位于src/go/runtime/chan.go的chansend方法,詳細(xì)執(zhí)行邏輯已通過注釋寫明。
func closechan(c *hchan) {
// 如果hchan對象為nil,則會引發(fā)painc
if c == nil {
panic(plainError("close of nil channel"))
}
// 對hchan加鎖
lock(&c.lock)
// 不同多次調(diào)用close(c chan<- Type)方法,否則會引發(fā)painc
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
if raceenabled {
callerpc := getcallerpc()
racewritepc(c.raceaddr(), callerpc, funcPC(closechan))
racerelease(c.raceaddr())
}
// close標(biāo)志
c.closed = 1
// gList代表Go的GMP調(diào)度的G集合
var glist gList
// 該for循環(huán)是為了釋放recvq上的所有等待接收sudog
for {
sg := c.recvq.dequeue()
if sg == nil {
break
}
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
}
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = nil
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
// 該for循環(huán)會釋放sendq上的所有等待發(fā)送sudog
for {
sg := c.sendq.dequeue()
if sg == nil {
break
}
sg.elem = nil
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = nil
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
// 釋放sendq和recvq之后,hchan釋放鎖
unlock(&c.lock)
// 將上文中g(shù)list中的加入的goroutine取出,讓它們均變?yōu)閞unnable(可執(zhí)行)狀態(tài),等待調(diào)度器執(zhí)行
// 注意:我們上文中分析過,試圖向一個已關(guān)閉的channel發(fā)送數(shù)據(jù),會引發(fā)painc。
// 所以,如果是釋放sendq中的goroutine,它們一旦得到執(zhí)行將會引發(fā)panic。
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
關(guān)于關(guān)閉操作,有幾個點(diǎn)需要注意一下。
- 如果關(guān)閉已關(guān)閉的channel會引發(fā)painc。
- 對channel關(guān)閉后,如果有阻塞的讀取或發(fā)送goroutines將會被喚醒。讀取goroutines會獲取到hchan的已接收元素,如果沒有,則獲取到元素零值;發(fā)送goroutine的執(zhí)行則會引發(fā)painc。
對于第二點(diǎn),我們可以很好利用這一特性來實(shí)現(xiàn)對程序執(zhí)行流的控制(類似于sync.WaitGroup的作用),以下是示例程序代碼。
func main() {
ch := make(chan struct{})
//
go func() {
// do something work...
// when work has done, call close()
close(ch)
}()
// waiting work done
<- ch
// other work continue...
}
四、總結(jié)
channel是Go中非常強(qiáng)大有用的機(jī)制,為了更有效地使用它,我們必須了解它的實(shí)現(xiàn)原理,這也是寫作本文的目的。
- hchan結(jié)構(gòu)體有鎖的保證,對于并發(fā)goroutine而言是安全的
- channel接收、發(fā)送數(shù)據(jù)遵循FIFO(First In First Out)原語
- channel的數(shù)據(jù)傳遞依賴于內(nèi)存拷貝
- channel能阻塞(gopark)、喚醒(goready)goroutine
- 所謂無緩存的channel,它的工作方式就是直接發(fā)送goroutine拷貝數(shù)據(jù)給接收goroutine,而不通過hchan.buf
另外,可以看到Go在channel的設(shè)計(jì)上權(quán)衡了簡單與性能。為了簡單性,hchan是有鎖的結(jié)構(gòu),因?yàn)橛墟i的隊(duì)列會更易理解和實(shí)現(xiàn),但是這樣會損失一些性能??紤]到整個 channel 操作帶鎖的成本較高,其實(shí)官方也曾考慮過使用無鎖 channel 的設(shè)計(jì),但是由于目前已有提案中(https://github.com/golang/go/issues/8899),無鎖實(shí)現(xiàn)的channel可維護(hù)性差、且實(shí)際性能測試不具有說服力,而且也不符合Go的簡單哲學(xué),因此官方目前為止并沒有采納無鎖設(shè)計(jì)。
在性能上,有一點(diǎn),我們需要認(rèn)識到:所謂channel中阻塞goroutine,只是在runtime系統(tǒng)中被blocked,它是用戶層的阻塞。而實(shí)際的底層內(nèi)核線程不受影響,它仍然是unblocked的。
參考鏈接
https://speakerdeck.com/kavya719/understanding-channels
https://codeburst.io/diving-deep-into-the-golang-channels-549fd4ed21a8