go channal是如何实现的
我们先通过golang汇编追踪下 make(chan,int)到底做了什么。
package main
import "fmt"
func main() {
ch := make(chan int,1)
go func() {
for {
ch <-1
}
}()
ret := <- ch
fmt.Println(ret)
}
go build --gcflags="-S" main.go
看到如下这段go汇编代码
LEAQ type.chan int(SB), AX //获取type.chan int 类型指针
MOVQ AX, (SP) //压栈 make的第一个参数
MOVQ $1, 8(SP) //压栈 make的第一个参数
CALL runtime.makechan(SB) //实际调用 runtime.makechan函数
那么ch := make(chan int,1)
其实是调用 runtime.makechan的方法,
go 1.13 源码
func makechan(t *chantype, size int) *hchan {
elem := t.elem
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"))
}
var c *hchan
switch {
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, elemblockevent, true)
}
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
return c
}
返回的类型是*hchan。所以chan是一个指针类型,这样子chan在各个goroutine的传递 都是直接传chan,传递chan 指针。
我看从go源码的(src/runtime/chan.go)看到的hchan struct
type hchan struct {
qcount uint // 当前使用的个数
dataqsiz uint // 缓冲区大小 make的第二个参数
buf unsafe.Pointer // 缓存数组的指针,带缓冲区的chan指向缓冲区,无缓存的channel指向自己指针(仅做数据竞争分析使用)
elemsize uint16 // 元素size
closed uint32 //是否已经关闭 1已经关闭 0还没关闭
elemtype *_type // 元素类型
sendx uint // 发送的索引 比如缓冲区大小为3 这个索引 经历 0-1-2 然后再从0开始
recvx uint // 接收的索引
recvq waitq // 等待recv的goroutine链表
sendq waitq // 等待send的goroutine链表
lock mutex //互斥锁
}
ret := <- ch
这个语句对于汇编是
LEAQ ""..autotmp_12+64(SP), CX
MOVQ CX, 8(SP)
CALL runtime.chanrecv2(SB)
我们看到源码中用 chanrecv1
和 chanrecv2
两个函数:
// entry points for <- c from compiled code
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
//go:nosplit
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c, elem, true)
return
}
我们这边对应的是 chanrecv2
, 那么什么时候是chanrecv1
?什么时候是chanrecv2
呢?
当 <- ch
chan左边没有接收值的时候使用的是 chanrecv1
,当左边有接收值的时候是chanrecv2
,注意 chan 的接收值有两个
ret := <- ch
//带bool值
ret,ok := <- ch
这两个都是使用chanrecv2
。
ch <-1
对应的汇编是
LEAQ ""..stmp_0(SB), CX
MOVQ CX, 8(SP)
CALL runtime.chansend1(SB)
这边可以看到实际上是调用runtime.chansend1
源码如下
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
实现上是调用runtime.chansend
获取chan的关闭状态
通过什么的一些源码阅读我们来实现下chan关闭状态的获取。
package main
import (
"fmt"
"unsafe"
)
func main() {
c := make(chan int, 10)
fmt.Println(isClosed(&c))
close(c)
fmt.Println(isClosed(&c))
}
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
}
func isClosed(c *chan int) uint32 {
a := unsafe.Pointer(c)
d := (**hchan)(a)
return (*d).closed
}
# go run main.go
0
1