Hi 你好,我是k哥。大廠搬磚6年的後端程式設計師。
我們知道,Go語言為了方便使用者,提供了簡單、安全的協程資料同步和通訊機制,channel。那我們知道channel底層是如何實現的嗎?今天k哥就來聊聊channel的底層實現原理。同時,為了驗證我們是否掌握了channel的實現原理,本文也收集了channel的高頻面試題,理解了原理,面試題自然不在話下。
1 原理
預設情況下,讀寫未就緒的channel(讀沒有資料的channel,或者寫緩衝區已滿的channel)時,協程會被阻塞。
但是當讀寫channel操作和select搭配使用時,即使channel未就緒,也可以執行其它分支,當前協程不會被阻塞。
ch := make(chan int)
select{
case <- ch:
default:
}
本文主要介紹channel的阻塞模式,和select搭配使用的非阻塞模式,後續會另起一篇介紹。
1.1 資料結構
channel涉及到的核心資料結構包含3個。
hchan
// channel
type hchan struct {
// 迴圈佇列
qcount uint // 通道中資料個數
dataqsiz uint // buf長度
buf unsafe.Pointer // 陣列指標
sendx uint // send index
recvx uint // receive index
elemsize uint16 // 元素大小
elemtype *_type // 元素型別
closed uint32 // 通道關閉標誌
recvq waitq // 由雙向連結串列實現的recv waiters佇列
sendq waitq // 由雙向連結串列實現的send waiters佇列
lock mutex
}
hchan是channel底層的資料結構,其核心是由陣列實現的一個環形緩衝區:
-
qcount 通道中資料個數
-
dataqsiz 陣列長度
-
buf 指向陣列的指標,陣列中儲存往channel傳送的資料
-
sendx 傳送元素到陣列的index
-
recvx 從陣列中接收元素的index
-
elemsize channel中元素型別的大小
-
elemtype channel中的元素型別
-
closed 通道關閉標誌
-
recvq 因讀取channel而陷入阻塞的協程等待佇列
-
sendq 因傳送channel而陷入阻塞的協程等待佇列
-
lock 鎖
waitq
// 等待佇列(雙向連結串列)
type waitq struct {
first *sudog
last *sudog
}
waitq是因讀寫channel而陷入阻塞的協程等待佇列。
-
first 佇列頭部
-
last 佇列尾部
sudog
// sudog represents a g in a wait list, such as for sending/receiving
// on a channel.
type sudog struct {
g *g // 等待send或recv的協程g
next *sudog // 等待佇列下一個結點next
prev *sudog // 等待佇列前一個結點prev
elem unsafe.Pointer // data element (may point to stack)
success bool // 標記協程g被喚醒是因為資料傳遞(true)還是channel被關閉(false)
c *hchan // channel
}
sudog是協程等待佇列的節點:
-
g 因讀寫而陷入阻塞的協程
-
next 等待佇列下一個節點
-
prev 等待佇列前一個節點
-
elem 對於寫channel,表示需要傳送到channel的資料指標;對於讀channel,表示需要被賦值的資料指標。
-
success 標記協程被喚醒是因為資料傳遞(true)還是channel被關閉(false)
-
c 指向channel的指標
1.2 通道建立
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// buf陣列所需分配記憶體大小
mem := elem.size*uintptr(size)
var c *hchan
switch {
case mem == 0:// Unbuffered channels,buf無需記憶體分配
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector uses this location for synchronization.
c.buf = c.raceaddr()
case elem.ptrdata == 0: // Buffered channels,通道元素型別非指標
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// Buffered channels,通道元素型別是指標
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
return c
}
通道建立主要是分配記憶體並構建hchan物件。
1.3 通道寫入
3種異常情況處理
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 1.channel為nil
if c == nil {
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
lock(&c.lock) //加鎖
// 2.如果channel已關閉,直接panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// Block on the channel.
mysg := acquireSudog()
c.sendq.enqueue(mysg) // 入sendq等待佇列
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
closed := !mysg.success // 協程被喚醒的原因是因為資料傳遞還是通道被關閉
// 3.因channel被關閉導致阻塞寫協程被喚醒並panic
if closed {
panic(plainError("send on closed channel"))
}
}
-
對 nil channel寫入,會死鎖
-
對被關閉的channel寫入,會panic
-
對因寫入而陷入阻塞的協程,如果channel被關閉,阻塞協程會被喚醒並panic
寫時有阻塞讀協程
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
lock(&c.lock) //加鎖
// 1、當存在等待接收的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) // 直接把正在傳送的值傳送給等待接收的Goroutine,並將此接收協程放入可排程佇列等待排程
return true
}
}
// send processes a send operation on an empty channel c.
// The value ep sent by the sender is copied to the receiver sg.
// The receiver is then woken up to go on its merry way.
// Channel c must be empty and locked. send unlocks c with unlockf.
// sg must already be dequeued from c.
// ep must be non-nil and point to the heap or the caller's stack.
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// 將ep寫入sg中的elem
if sg.elem != nil {
t:=c.elemtype
dst := sg.elem
// memmove copies n bytes from "from" to "to".
memmove(dst, ep, t.size)
sg.elem = nil // 資料已經被寫入到<- c變數,因此sg.elem指標可以置空了
}
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
sg.success = true
// 喚醒receiver協程gp
goready(gp, skip+1)
}
// 喚醒receiver協程gp,將其放入可執行佇列中等待排程執行
func goready(gp *g, traceskip int) {
systemstack(func() {
ready(gp, traceskip, true)
})
}
// Mark gp ready to run.
func ready(gp *g, traceskip int, next bool) {
status := readgstatus(gp)
// Mark runnable.
_g_ := getg()
mp := acquirem() // disable preemption because it can be holding p in a local var
// status is Gwaiting or Gscanwaiting, make Grunnable and put on runq
casgstatus(gp, _Gwaiting, _Grunnable)
runqput(_g_.m.p.ptr(), gp, next)
wakep()
releasem(mp)
}
-
加鎖
-
從阻塞讀協程佇列取出sudog節點
-
在send方法中,呼叫memmove方法將資料複製給sudog.elem指向的變數。
-
goready方法喚醒接收到資料的阻塞讀協程g,將其放入協程可執行佇列中等待排程
-
解鎖
寫時無阻塞讀協程但環形緩衝區仍有空間
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
lock(&c.lock) //加鎖
// 當緩衝區未滿時
if c.qcount < c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
qp := chanbuf(c, c.sendx) // 獲取指向緩衝區陣列中位於sendx位置的元素的指標
typedmemmove(c.elemtype, qp, ep) // 將當前傳送的值複製到緩衝區
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0 // 因為是迴圈佇列,sendx等於佇列長度時置為0
}
c.qcount++
unlock(&c.lock)
return true
}
}
-
加鎖
-
將資料放入環形緩衝區
-
解鎖
寫時無阻塞讀協程且環形緩衝區無空間
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
lock(&c.lock) //加鎖
// Block on the channel.
// 將當前的Goroutine打包成一個sudog節點,並加入到阻塞寫佇列sendq裡
gp := getg()
mysg := acquireSudog()
mysg.elem = ep
mysg.g = gp
mysg.c = c
gp.waiting = mysg
c.sendq.enqueue(mysg) // 入sendq等待佇列
// 呼叫gopark將當前Goroutine設定為等待狀態並解鎖,進入休眠等待被喚醒,觸發協程排程
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
// 被喚醒之後執行清理工作並釋放sudog結構體
gp.waiting = nil
gp.activeStackChans = false
closed := !mysg.success // gp被喚醒的原因是因為資料傳遞還是通道被關閉
gp.param = nil
mysg.c = nil
releaseSudog(mysg)
// 因關閉被喚醒則panic
if closed {
panic(plainError("send on closed channel"))
}
// 資料成功傳遞
return true
}
-
加鎖。
-
將當前協程gp封裝成sudog節點,並加入channel的阻塞寫佇列sendq。
-
呼叫gopark將當前協程設定為等待狀態並解鎖,觸發排程其它協程執行。
-
因資料被讀或者channel被關閉,協程從park中被喚醒,清理sudog結構。
-
因channel被關閉導致協程喚醒,panic
-
返回
整體寫流程
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 1.channel為nil
if c == nil {
// 當前Goroutine阻塞掛起
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 2.加鎖
lock(&c.lock)
// 3.如果channel已關閉,直接panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// 4、存在阻塞讀協程
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) // 直接把正在傳送的值傳送給等待接收的Goroutine,並將此接收協程放入可排程佇列等待排程
return true
}
// 5、緩衝區未滿時
if c.qcount < c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
qp := chanbuf(c, c.sendx) // 獲取指向緩衝區陣列中位於sendx位置的元素的指標
typedmemmove(c.elemtype, qp, ep) // 將當前傳送的值複製到緩衝區
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0 // 因為是迴圈佇列,sendx等於佇列長度時置為0
}
c.qcount++
unlock(&c.lock)
return true
}
// Block on the channel.
// 6、將當前協程打包成一個sudog結構體,並加入到channel的阻塞寫佇列sendq
gp := getg()
mysg := acquireSudog()
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg) // 入sendq等待佇列
atomic.Store8(&gp.parkingOnChan, 1)
// 7.呼叫gopark將當前協程設定為等待狀態並解鎖,進入休眠,等待被喚醒,並觸發協程排程
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
// 8. 被喚醒之後執行清理工作並釋放sudog結構體
gp.waiting = nil
gp.activeStackChans = false
closed := !mysg.success // g被喚醒的原因是因為資料傳遞還是通道被關閉
gp.param = nil
mysg.c = nil
releaseSudog(mysg)
// 9.因關閉被喚醒則panic
if closed {
panic(plainError("send on closed channel"))
}
// 10.資料成功傳遞
return true
}
-
channel為nil檢查。為空則死鎖。
-
加鎖
-
如果channel已關閉,直接panic。
-
當存在阻塞讀協程,直接把資料傳送給讀協程,喚醒並將其放入協程可執行佇列中等待排程執行。
-
當緩衝區未滿時,將當前傳送的資料複製到緩衝區。
-
當既沒有阻塞讀協程,緩衝區也沒有剩餘空間時,將協程加入阻塞寫佇列sendq。
-
呼叫gopark將當前協程設定為等待狀態,進入休眠等待被喚醒,觸發協程排程。
-
被喚醒之後執行清理工作並釋放sudog結構體
-
喚醒之後檢查,因channel被關閉導致協程喚醒則panic。
-
返回。
1.4 通道讀
2種異常情況處理
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// 1.channel為nil
if c == nil {
// 否則,當前Goroutine阻塞掛起
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
lock(&c.lock)
// 2.如果channel已關閉,並且緩衝區無元素,返回(true,false)
if c.closed != 0 {
if c.qcount == 0 {
unlock(&c.lock)
if ep != nil {
//根據channel元素的型別清理ep對應地址的記憶體,即ep接收了channel元素型別的零值
typedmemclr(c.elemtype, ep)
}
return true, false
}
}
}
-
channel未初始化,讀操作會死鎖
-
channel已關閉且緩衝區無資料,給讀變數賦零值。
讀時有阻塞寫協程
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
lock(&c.lock)
// Just found waiting sender with not closed.
// 等待傳送的佇列sendq裡存在Goroutine
if sg := c.sendq.dequeue(); sg != nil {
// Found a waiting sender. If buffer is size 0, receive value
// directly from sender. Otherwise, receive from head of queue
// and add sender's value to the tail of the queue (both map to
// the same buffer slot because the queue is full).
// 如果無緩衝區,那麼直接從sender接收資料;否則,從buf佇列的頭部接收資料,並把sender的資料加到buf佇列的尾部
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true // 接收成功
}
}
// recv processes a receive operation on a full channel c.
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// channel無緩衝區,直接從sender讀
if c.dataqsiz == 0 {
if ep != nil {
// copy data from sender
t := c.elemtype
src := sg.elem
typeBitsBulkBarrier(t, uintptr(ep), uintptr(src), t.size)
memmove(dst, src, t.size)
}
} else {
// 從佇列讀,sender再寫入佇列
qp := chanbuf(c, c.recvx)
// copy data from queue to receiver
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// copy data from sender to queue
typedmemmove(c.elemtype, qp, sg.elem)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
// 喚醒sender佇列協程sg
sg.elem = nil
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
sg.success = true
// 喚醒協程
goready(gp, skip+1)
}
-
加鎖
-
從阻塞寫佇列取出sudog節點
-
假如channel為無緩衝區通道,則直接讀取sudog對應寫協程資料,喚醒寫協程。
-
假如channel為緩衝區通道,從channel緩衝區頭部(recvx)讀資料,將sudog對應寫協程資料,寫入緩衝區尾部(sendx),喚醒寫協程。
-
解鎖
讀時無阻塞寫協程且緩衝區有資料
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
lock(&c.lock)
// 緩衝區buf中有元素,直接從buf複製元素到當前協程(在已關閉的情況下,佇列有資料依然會讀)
if c.qcount > 0 {
// Receive directly from queue
qp := chanbuf(c, c.recvx)
if ep != nil {
typedmemmove(c.elemtype, ep, qp)// 將從buf中取出的元素複製到當前協程
}
typedmemclr(c.elemtype, qp) // 同時將取出的資料所在的記憶體清空
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
unlock(&c.lock)
return true, true // 接收成功
}
}
-
加鎖
-
從環形緩衝區讀資料。在channel已關閉的情況下,緩衝區有資料依然可以被讀。
-
解鎖
讀時無阻塞寫協程且緩衝區無資料
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
lock(&c.lock)
// no sender available: block on this channel.
// 阻塞模式,獲取當前Goroutine,打包一個sudog,並加入到channel的接收佇列recvq裡
gp := getg()
mysg := acquireSudog()
mysg.elem = ep
gp.waiting = mysg
mysg.g = gp
mysg.c = c
gp.param = nil
c.recvq.enqueue(mysg) // 入接收佇列recvq
// 掛起當前Goroutine,設定為_Gwaiting狀態,進入休眠等待被喚醒
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
// 因通道關閉或者讀到資料被喚醒
gp.waiting = nil
success := mysg.success
gp.param = nil
mysg.c = nil
releaseSudog(mysg)
return true, success // 10.返回成功
}
-
加鎖。
-
將當前協程gp封裝成sudog節點,加入channel的阻塞讀佇列recvq。
-
呼叫gopark將當前協程設定為等待狀態並解鎖,觸發排程其它協程執行。
-
因讀到資料或者channel被關閉,協程從park中被喚醒,清理sudog結構。
-
返回
整體讀流程
// chanrecv receives on channel c and writes the received data to ep.
// ep may be nil, in which case received data is ignored.
// If block == false and no elements are available, returns (false, false).
// Otherwise, if c is closed, zeros *ep and returns (true, false).
// Otherwise, fills in *ep with an element and returns (true, true).
// A non-nil ep must point to the heap or the caller's stack.
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// 1.channel為nil
if c == nil {
// 否則,當前Goroutine阻塞掛起
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 2.加鎖
lock(&c.lock)
// 3.如果channel已關閉,並且緩衝區無元素,返回(true,false)
if c.closed != 0 {
if c.qcount == 0 {
unlock(&c.lock)
if ep != nil {
//根據channel元素的型別清理ep對應地址的記憶體,即ep接收了channel元素型別的零值
typedmemclr(c.elemtype, ep)
}
return true, false
}
// The channel has been closed, but the channel's buffer have data.
} else {
// Just found waiting sender with not closed.
// 4.存在阻塞寫協程
if sg := c.sendq.dequeue(); sg != nil {
// Found a waiting sender. If buffer is size 0, receive value
// directly from sender. Otherwise, receive from head of queue
// and add sender's value to the tail of the queue (both map to
// the same buffer slot because the queue is full).
// 如果無緩衝區,那麼直接從sender接收資料;否則,從buf佇列的頭部接收資料,並把sender的資料加到buf佇列的尾部
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true // 接收成功
}
}
// 5.緩衝區buf中有元素,直接從buf複製元素到當前協程(在已關閉的情況下,佇列有資料依然會讀)
if c.qcount > 0 {
// Receive directly from queue
qp := chanbuf(c, c.recvx)
if ep != nil {
typedmemmove(c.elemtype, ep, qp)// 將從buf中取出的元素複製到當前協程
}
typedmemclr(c.elemtype, qp) // 同時將取出的資料所在的記憶體清空
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
unlock(&c.lock)
return true, true // 接收成功
}
// no sender available: block on this channel.
// 6.獲取當前Goroutine,封裝成sudog節點,加入channel阻塞讀佇列recvq
gp := getg()
mysg := acquireSudog()
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.c = c
gp.param = nil
c.recvq.enqueue(mysg) // 入接收佇列recvq
atomic.Store8(&gp.parkingOnChan, 1)
// 7.掛起當前Goroutine,設定為_Gwaiting狀態,進入休眠等待被喚醒
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
// 8.因通道關閉或者可讀被喚醒
gp.waiting = nil
gp.activeStackChans = false
success := mysg.success
gp.param = nil
mysg.c = nil
releaseSudog(mysg)
// 9.返回
return true, success
}
通道讀流程如下:
-
channel為nil檢查。空則死鎖。
-
加鎖。
-
如果channel已關閉,並且緩衝區無資料,讀變數賦零值,返回。
-
當存在阻塞寫協程,如果緩衝區已滿,則直接從sender接收資料;否則,從環形緩衝區頭部接收資料,並把sender的資料加到環形緩衝區尾部。喚醒sender,將其放入協程可執行佇列中等待排程執行,返回。
-
如果緩衝區中有資料,直接從緩衝區複製資料到當前協程,返回。
-
當既沒有阻塞寫協程,緩衝區也沒有資料時,將協程加入阻塞讀佇列recvq。
-
呼叫gopark將當前協程設定為等待狀態,進入休眠等待被喚醒,觸發協程排程。
-
因通道關閉或者可讀被喚醒。
-
返回。
1.5 通道關閉
func closechan(c *hchan) {
// // 1.channel為nil則panic
if c == nil {
panic(plainError("close of nil channel"))
}
lock(&c.lock)
// 2.已關閉的channel再次關閉則panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
// 設定關閉標記
c.closed = 1
var glist gList
// 遍歷recvq和sendq中的協程放入glist
// release all readers
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 = unsafe.Pointer(sg)
sg.success = false
glist.push(gp)
}
// release all writers (they will panic)
for {
sg := c.sendq.dequeue()
if sg == nil {
break
}
sg.elem = nil
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = unsafe.Pointer(sg)
sg.success = false
glist.push(gp)
}
unlock(&c.lock)
// 3.將glist中所有Goroutine的狀態置為_Grunnable,等待排程器進行排程
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
-
channel為nil檢查。為空則panic
-
已關閉channel再次被關閉,panic
-
將sendq和recvq所有Goroutine的狀態置為_Grunnable,放入協程排程佇列等待排程器排程
2 高頻面試題
-
channel 的底層實現原理 (資料結構)
-
nil、關閉的 channel、有資料的 channel,再進行讀、寫、關閉會怎麼樣?(各類變種題型)
-
有緩衝channel和無緩衝channel的區別
原文連結:https://reurl.cc/Wx26jD