channel存在以下四个特性
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)
}
}
基于的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)
从上面可以看出
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
}
从上面的汇编可以看到,当执行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
}
从上面的汇编可以看到,当执行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
}
从上面的汇编可以看到,当执行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
}
从上面的汇编可以看到,当执行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接收元素,
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())
}
对于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
}
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