文章目錄
channels
channel存在以下四個特性
- goroutine-safe
- store and pass values between goroutines
- provide FIFO semantics
- can cause goroutines to block and unblock
How to use Channels
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 3)
go func() {
for i := 0; i < 10; i++ {
ch <- i
time.Sleep(500 * time.Millisecond)
}
close(ch)
}()
value := <-ch
fmt.Println(value)
for value := range ch {
fmt.Println(value)
}
}
Channel源碼解析
基於的go版本爲1.13,源代碼位於runtime/chan.go
代碼入口
go的編譯器對channel進行處理,從彙編中可以看到具體調用的函數。下面截取了部分的關鍵代碼。
$ go tool compile -S channel.go > channel.s
$ vim channel.s
0x0035 00053 (channel.go:9) CALL runtime.makechan(SB)
0x003e 00062 (channel.go:12) CALL runtime.chansend1(SB)
0x0078 00120 (channel.go:18) CALL runtime.chanrecv1(SB)
0x0067 00103 (channel.go:15) CALL runtime.closechan(SB)
從上面可以看出
- 當調用make(chan int,4)時,調用的是runtime下的makechan函數
- 當調用ch <- i時,調用的是runtime下的chansend1函數
- 當調用value := <-ch時,調用的是runtime下的chanrecv函數
channel的結構體
type hchan struct {
qcount uint // channel的隊列中數據的總數,會隨着<-和 -> 變化
dataqsiz uint // channel循環數組的長度,
buf unsafe.Pointer // 指向底層循環數組的指針,只針對緩衝channel
elemsize uint16 //元素大小
closed uint32 //channel是否被關閉的標誌
elemtype *_type // 元素類型
sendx uint // 發送元素在循環數組中的索引
recvx uint // 接收元素在循環數組中的索引
recvq waitq // 等待接收的goroutine隊列
sendq waitq // 等待發送的goroutine隊列
//保護hchan中的所有字段以及recvq,sendq中的sudogs
lock mutex
}
//waitq是隊列,用於存儲sudog
type waitq struct {
first *sudog
last *sudog
}
新建channel
從上面的彙編可以看到,當執行make指令時,go會調用makechan函數來創建一個hchan的結構體
輸入的參數是channel的類型和長度,返回一個指向hchan的指針
const (
hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1))
)
//malloc.go
func func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// 忽略一些前置檢查,分析關鍵部分的代碼
...
//MulUintptr返回elem.size*uintptr(size)的值,並判斷是否溢出
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
var c *hchan
switch {
case mem == 0:
// mem爲0,說明該channel是無緩衝,申請的長度爲hchanSize(hchan的基本長度)
c = (*hchan)(mallocgc(hchanSize, nil, true))
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// elem中並不存在指針,申請的總長度即爲hchanSize+mem
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
//buf指向c+hchanSize的位置
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// elem中包含指針
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
//複製elem的大小,類型,並設置循環數組的總長度
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
//
...
return c
}
channel發送元素
從上面的彙編可以看到,當執行ch <- i 指令時,go會調用chansend1函數,從函數可以看出,chansend1實際調用的是chansend函數。
// entry point for c <- x from compiled code
//go:nosplit
//go:nosplit用來指定文件中聲明的函數不得進行堆棧溢出檢查,這是一個在不安全搶佔調用goroutine時常用的做法。
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
//如果channel爲空
if c == nil {
//如果是非阻塞的,直接返回false
if !block {
return false
}
//當前的goroutine被掛起
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
//對於非阻塞情況,進行fast check的操作
//當滿足條件爲 1.非阻塞 2.channel未關閉 3.無緩衝channel&&沒有等待的goroutine 4.緩衝channel並且循環數組已經滿了
//則直接返回false
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)
//如果channel已經被關閉,則拋出異常
//考點:往關閉的channel發送數據,會導致拋出異常
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
//如果channel的等待goroutine隊列不爲空,說明有goroutine在等待接收值
if sg := c.recvq.dequeue(); sg != nil {
//將ep的值進行直接拷貝,繞過channel的buffer緩存
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
//如果當前循環數組還沒滿,則將元素入隊
if c.qcount < c.dataqsiz {
//返回sendx指向的指針位置
qp := chanbuf(c, c.sendx)
//將元素進行拷貝
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
}
//如果爲阻塞channel,並且當前的循環數組已經滿了,則將當前的gouroutine封裝成sudog入隊。
//獲取當前goroutine
gp := getg()
//獲取一個sudog
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
//將goroutine對應的sudog入隊,並掛起goroutine
c.sendq.enqueue(mysg)
goparkunlock(&c.lock, waitReasonChanSend, traceEvGoBlockSend, 3)
KeepAlive(ep)
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讀取元素
從上面的彙編可以看到,當執行ch <- i 指令時,go會調用chanrecv1函數,從函數可以看出,chanrecv1實際調用的是chanrecv函數。核心邏輯和發送元素一致。
// entry points for <- c from compiled code
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
// 如果block == false 並且沒有對應的元素, 則返回(false, false).
// 如果channel被關閉了, 則對ep的元素置零,並返回 (true, false).
// 否則將指針ep指向的值進行賦值並返回(true, true)
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
//如果channel爲空
if c == nil {
//該channel爲非阻塞
if !block {
return
}
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
//對於非阻塞情況,進行fast check的操作
//當滿足條件爲 1.非阻塞 2.非緩衝channel並且沒有等待發送的goroutine
//3.緩衝channel,但是循環數組爲空 4.channel未關閉
//則直接返回false
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)
//如果當前channel已經被關閉,並且循環數組爲空,
if c.closed != 0 && c.qcount == 0 {
unlock(&c.lock)
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
//如果發送隊列中存在等待的goroutine,則將sg中的值直接複製給ep
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
// 如果當前循環數組不爲空
if c.qcount > 0 {
//返回recvx指向的指針位置
qp := chanbuf(c, c.recvx)
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
}
//如果爲非阻塞的channel,直接返回
if !block {
unlock(&c.lock)
return false, false
}
//和send一樣,將當前的goroutine封裝成sudog,併入隊
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
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
從上面的彙編可以看到,當執行ch <- i 指令時,go會調用closechan函數
func closechan(c *hchan) {
//如果channel爲空,則拋出panic
if c == nil {
panic(plainError("close of nil channel"))
}
lock(&c.lock)
//如果當前channel已經被關閉了,則會拋出異常
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
//設置closed標誌位爲1
c.closed = 1
var glist gList
// 將recv隊列中的sudog進行出隊,並將elem置空,將gouroutine放入glist列表中
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
glist.push(gp)
}
// 同理,將send隊列中的sudog出隊,將elem置空,並將goroutine放入glist中
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
glist.push(gp)
}
unlock(&c.lock)
// 將從上面獲取的所有goroutine的狀態置爲Ready
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
select channel
channel reveive value
對於select下的channel接收元素,
select {
case c <- v:
... foo
default:
... bar
}
go的編譯器將它轉換成selectnbsend
if selectnbsend(c, v) {
... foo
} else {
... bar
}
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
return chansend(c, elem, false, getcallerpc())
}
channel send value
對於select下的發送value場景
select {
case v = <-c:
... foo
default:
... bar
}
go的編譯器將它轉換成selectnbrevc
if selectnbrecv(&v, c) {
... foo
} else {
... bar
}
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
selected, _ = chanrecv(c, elem, false)
return
}
用於判斷channel是否關閉的場景
select {
case v, ok = <-c:
... foo
default:
... bar
}
golang編譯器會將它轉換成selectnbrecv2
if c != nil && selectnbrecv2(&v, &ok, c) {
... foo
} else {
... bar
}
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
}
https://speakerd.s3.amazonaws.com/presentations/10ac0b1d76a6463aa98ad6a9dec917a7/GopherCon_v10.0.pdf