1.簡介
channel是Go語言的一大特性,基于channel有很多值得探討的問題,如
- channel為什么是并發安全的?
- 同步通道和異步通道有啥區別?
- 通道為何會阻塞協程?
- 使用通道導致阻塞的協程是如何解除阻塞的?
要了解本質,需要進源碼查看,畢竟源碼之下了無秘密。
2.原理
2.1創建
channel理論上有三種,帶緩沖不帶緩沖nil,寫法如下:
// buffered
ch := make(chan Task, 3)
// unbuffered
ch := make(chan int)
// nil
var ch chan int
復制代碼
追蹤make函數,會發現在builtin/builtin.go中僅有一個聲明func make(t Type, size ...IntegerType) Type。真正的實現可以參考go內置函數make,簡單來說在
cmd/compile/internal/gc/typecheck.go中有函數typecheck1
// The result of typecheck1 MUST be assigned back to n, e.g.
// n.Left = typecheck1(n.Left, top)
func typecheck1(n *Node, top int) (res *Node) {
if enableTrace && trace {
defer tracePrint("typecheck1", n)(&res)
}
switch n.Op {
case OMAKE:
ok |= ctxExpr
args := n.List.Slice()
if len(args) == 0 {
yyerror("missing argument to make")
n.Type = nil
return n
}
n.List.Set(nil)
l := args[0]
l = typecheck(l, Etype)
t := l.Type
if t == nil {
n.Type = nil
return n
}
i := 1
switch t.Etype {
default:
yyerror("cannot make type %v", t)
n.Type = nil
return n
case TCHAN:
l = nil
if i < len(args) {
l = args[i]
i++
l = typecheck(l, ctxExpr)
l = defaultlit(l, types.Types[TINT])
if l.Type == nil {
n.Type = nil
return n
}
if !checkmake(t, "buffer", l) {
n.Type = nil
return n
}
n.Left = l
} else {
n.Left = nodintconst(0)
}
n.Op = OMAKECHAN //對應的函數位置
}
if i < len(args) {
yyerror("too many arguments to make(%v)", t)
n.Op = OMAKE
n.Type = nil
return n
}
n.Type = t
if (top&ctxStmt != 0) && top&(ctxCallee|ctxExpr|Etype) == 0 && ok&ctxStmt == 0 {
if !n.Diag() {
yyerror("%v evaluated but not used", n)
n.SetDiag(true)
}
n.Type = nil
return n
}
return n
}
}
復制代碼
最終真正實現位置為runtime/chan.go
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// compiler checks this but be safe.
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
// Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
// buf points into the same allocation, elemtype is persistent.
// SudoG's are referenced from their owning thread so they can't be collected.
// TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
var c *hchan
switch {
case mem == 0:
// Queue or element size is zero.
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector uses this location for synchronization.
c.buf = c.raceaddr()
case elem.kind&kindNoPointers != 0:
// Elements do not contain pointers.
// Allocate hchan and buf in one call.
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// Elements contain pointers.
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
if debugChan {
print("makechan: chan=", c, "; elemsize=", elem.size, "; elemalg=", elem.alg, "; dataqsiz=", size, "n")
}
return c
}
復制代碼
從這個函數可以看出,channel的數據結構為hchan
2.2結構
接下來我們看一下channel的數據結構,基于數據結構,可以推測出具體實現。
runtime/chan.go
type hchan struct {
//channel隊列里面總的數據量
qcount uint // total data in the queue
// 循環隊列的容量,如果是非緩沖的channel就是0
dataqsiz uint // size of the circular queue
// 緩沖隊列,數組類型。
buf unsafe.Pointer // points to an array of dataqsiz elements
// 元素占用字節的size
elemsize uint16
// 當前隊列關閉標志位,非零表示關閉
closed uint32
// 隊列里面元素類型
elemtype *_type // element type
// 隊列send索引
sendx uint // send index
// 隊列索引
recvx uint // receive index
// 等待channel的G隊列。
recvq waitq // list of recv waiters
// 向channel發送數據的G隊列。
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
// 全局鎖
lock mutex
}
復制代碼
通過該hchan的數據結構和makechan函數,數據結構里有幾個值得說明的數據:
- dataqsiz表示channel的長度,如果為非緩沖隊列,則值為0。通過dataqsiz實現環形隊列。
- buf存放真正的數據
- sendx和recvx指在環形隊列中數據入channel和出channel的位置
- sendq存放向channel發送數據的goroutine隊列
- recvq存放等待獲取channel數據的goroutine隊列
- lock為全局鎖
2.3Anwser
通過追查到的代碼,我們可以回答最開始提出的幾個問題了。
2.3.1channel為什么是并發安全的?
因為做操作之前,都會先獲取全局鎖,只有獲取成功的才能進行操作,保證了并發安全。
2.3.2同步通道和異步通道有啥區別?
使用的底層數據結構、操作代碼都是一樣的,只不過dataqsiz的值不一樣,一個為0,一個為正數。
2.3.3通道為何會阻塞協程?
當通道已經滿了,但協程繼續往通道里寫入,或者通道里沒有數據,但是協程從通道里獲取數據時,協程會被阻塞。
實現的原理與Golang并發調度的GMP模型強相關。
寫入滿通道的流程
- 當前goroutine(G1)創建自身的一個引用(sudog),放置到hchan的sendq隊列
- 當前goroutine(G1)會調用gopark函數,將當前協程置為waiting狀態;
- 將M和G1綁定關系斷開;
- scheduler會調度另外一個就緒態的goroutine與M建立綁定關系,然后M 會運行另外一個G。
讀取空通道的流程
- 當前goroutine(G2)會創建自身的一個引用(sudog)
- 將代表G2的sudog存入recvq等待隊列
- G2會調用gopark函數進入等待狀態,讓出OS thread,然后G2進入阻塞態
2.3.4使用通道導致阻塞的協程是如何解除阻塞的?
對于已經滿的通道,當有協程G2做讀操作時,會解除G1的阻塞,流程為
- G2調用 t:=<-ch 獲取一個元素A;
- 從hchan的buf里面取出一個元素;
- 從sendq等待隊列里面pop一個sudog;
- 將G1要寫入的數據復制到buf中A的位置,然后更新buf的sendx和recvx索引值;
- G2調用goready(G1)將G1置為Runable狀態,表示G1可以恢復運行;
對于讀取空的通道,當有協程G1做寫操作時,會解除G2的阻塞,流程為
- 將待寫入的消息發送給接收的goroutine G2;
- G1調用goready(G2) 將G2設置成就緒狀態,等待調度;
2.4實現
我們來看一下chan的具體實現
2.4.1讀取數據
// 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) {
// raceenabled: don't need to check ep, as it is always on the stack
// or is new memory allocated by reflect.
if debugChan {
print("chanrecv: chan=", c, "n")
}
if c == nil {
if !block {
return
}
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// Fast path: check for failed non-blocking operation without acquiring the lock.
//
// After observing that the channel is not ready for receiving, we observe that the
// channel is not closed. Each of these observations is a single word-sized read
// (first c.sendq.first or c.qcount, and second c.closed).
// Because a channel cannot be reopened, the later observation of the channel
// being not closed implies that it was also not closed at the moment of the
// first observation. We behave as if we observed the channel at that moment
// and report that the receive cannot proceed.
//
// The order of operations is important here: reversing the operations can lead to
// incorrect behavior when racing with a close.
if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
atomic.Load(&c.closed) == 0 {
return
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
lock(&c.lock)
if c.closed != 0 && c.qcount == 0 {
if raceenabled {
raceacquire(c.raceaddr())
}
unlock(&c.lock)
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
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).
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
if c.qcount > 0 {
// Receive directly from queue
qp := chanbuf(c, c.recvx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
}
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
typedmemclr(c.elemtype, qp)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
unlock(&c.lock)
return true, true
}
if !block {
unlock(&c.lock)
return false, false
}
// no sender available: block on this channel.
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
gp.waiting = mysg
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.param = nil
c.recvq.enqueue(mysg)
goparkunlock(&c.lock, waitReasonChanReceive, traceEvGoBlockRecv, 3)
// someone woke us up
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
closed := gp.param == nil
gp.param = nil
mysg.c = nil
releaseSudog(mysg)
return true, !closed
}
復制代碼
接收channel的數據的流程如下:
CASE1:前置channel為nil的場景:
如果block為非阻塞,直接return;
如果block為阻塞,就調用gopark()阻塞當前goroutine,并拋出異常。
- 前置場景,block為非阻塞,且channel為非緩沖隊列且sender等待隊列為空 或者 channel為有緩沖隊列但是隊列里面元素數量為0,且channel未關閉,這個時候直接return;
- 調用 lock(&c.lock) 鎖住channel的全局鎖;
CASE2:channel已經被關閉且channel緩沖中沒有數據了,這時直接返回success和空值;
CASE3:sender隊列非空,調用 func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int)
函數處理:
1.先取channel緩沖隊列的對頭元素復制給receiver(也就是ep);
2.將sender隊列的對頭元素里面的數據復制到channel緩沖隊列剛剛彈出的元素的位置,這樣緩沖隊列就不用移動數據了。
channel是非緩沖channel,直接調用recvDirect函數直接從sender recv元素到ep對象,這樣就只用復制一次;
對于sender隊列非空情況下, 有緩沖的channel的緩沖隊列一定是滿的:
釋放channel的全局鎖;
調用goready函數標記當前goroutine處于ready,可以運行的狀態;
CASE4:sender隊列為空,緩沖隊列非空,直接取隊列元素,移動頭索引;
CASE5:sender隊列為空、緩沖隊列也沒有元素且不阻塞協程,直接return (false,false);
CASE6:sender隊列為空且channel的緩存隊列為空,將goroutine加入recv隊列,并阻塞。
2.4.2寫入數據
/*
* generic single channel send/recv
* If block is not nil,
* then the protocol will not
* sleep but return if it could
* not complete.
*
* sleep can wake up with g.param == nil
* when a channel involved in the sleep has
* been closed. it is easiest to loop and re-run
* the operation; we'll see that it's now closed.
*/
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
if debugChan {
print("chansend: chan=", c, "n")
}
if raceenabled {
racereadpc(c.raceaddr(), callerpc, funcPC(chansend))
}
// Fast path: check for failed non-blocking operation without acquiring the lock.
//
// After observing that the channel is not closed, we observe that the channel is
// not ready for sending. Each of these observations is a single word-sized read
// (first c.closed and second c.recvq.first or c.qcount depending on kind of channel).
// Because a closed channel cannot transition from 'ready for sending' to
// 'not ready for sending', even if the channel is closed between the two observations,
// they imply a moment between the two when the channel was both not yet closed
// and not ready for sending. We behave as if we observed the channel at that moment,
// and report that the send cannot proceed.
//
// It is okay if the reads are reordered here: if we observe that the channel is not
// ready for sending and then observe that it is not closed, that implies that the
// channel wasn't closed during the first observation.
if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
return false
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
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
}
if c.qcount < c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
qp := chanbuf(c, c.sendx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
}
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
if !block {
unlock(&c.lock)
return false
}
// Block on the channel. Some receiver will complete our operation for us.
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)
goparkunlock(&c.lock, waitReasonChanSend, traceEvGoBlockSend, 3)
// Ensure the value being sent is kept alive until the
// receiver copies it out. The sudog has a pointer to the
// stack object, but sudogs aren't considered as roots of the
// stack tracer.
KeepAlive(ep)
// someone woke us up.
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
if gp.param == nil {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
releaseSudog(mysg)
return true
}
復制代碼
向channel寫入數據主要流程如下:
- CASE1:當channel為空或者未初始化,如果block表示阻塞那么向其中發送數據將會永久阻塞;如果block表示非阻塞就會直接return;
- CASE2:前置場景,block為非阻塞,且channel沒有關閉(已關閉的channel不能寫入數據)且(channel為非緩沖隊列且receiver等待隊列為空)或則( channel為有緩沖隊列但是隊列已滿),這個時候直接return;
- 調用 lock(&c.lock) 鎖住channel的全局鎖;
- CASE3:不能向已經關閉的channel send數據,會導致panic。
- CASE4:如果channel上的recv隊列非空,則跳過channel的緩存隊列,直接向消息發送給接收的goroutine:
- 調用sendDirect方法,將待寫入的消息發送給接收的goroutine;
- 釋放channel的全局鎖;
- 調用goready函數,將接收消息的goroutine設置成就緒狀態,等待調度。
- CASE5:緩存隊列未滿,則將消息復制到緩存隊列上,然后釋放全局鎖;
- CASE6:緩存隊列已滿且接收消息隊列recv為空,則將當前的goroutine加入到send隊列;
- 獲取當前goroutine的sudog,然后入channel的send隊列;
- 將當前goroutine休眠
關閉channel
func closechan(c *hchan) {
if c == nil {
panic(plainError("close of nil channel"))
}
lock(&c.lock)
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())
}
c.closed = 1
var glist 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 = nil
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
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 = nil
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
unlock(&c.lock)
// Ready all Gs now that we've dropped the channel lock.
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
復制代碼
關閉的主要流程如下所示:
- 獲取全局鎖;
- 設置channel數據結構chan的關閉標志位;
- 獲取當前channel上面的讀goroutine并鏈接成鏈表;
- 獲取當前channel上面的寫goroutine然后拼接到前面的讀鏈表后面;
- 釋放全局鎖;
- 喚醒所有的讀寫goroutine。
總結
了解一下具體實現還是很好的,雖然在使用上不會帶來變化,不過理解了內涵后,能夠更加靈活地使用通道,可以更加容易的追查到問題,也能學習到高手的設計思想。
資料
- Golang-Channel原理解析
- golang對于 nil通道 close通道你所不知道的神器特性
- Go語言make和new關鍵字的區別及實現原理
- Go底層引用實現
- 圖解Golang的channel底層原理
- go內置函數make
- Golang并發調度的GMP模型
最后
大家如果喜歡我的文章,可以關注我的公眾號(程序員麻辣燙)