多图详解Go中的Channel源码

本文使用的go的源码时14.4

chan介绍 package main import "fmt" func main() { c := make(chan int) go func() { c <- 1 // send to channel }() x := <-c // recv from channel fmt.Println(x) }

我们可以这样查看汇编结果:

go tool compile -N -l -S hello.go -N表示禁用优化 -l禁用内联 -S打印结果

通过上面这样的方式,我们可以直到chan是调用的哪些函数:

ch

源码分析 结构体与创建 type hchan struct { qcount uint // 循环列表元素个数 dataqsiz uint // 循环队列的大小 buf unsafe.Pointer // 循环队列的指针 elemsize uint16 // chan中元素的大小 closed uint32 // 是否已close elemtype *_type // chan中元素类型 sendx uint // send在buffer中的索引 recvx uint // recv在buffer中的索引 recvq waitq // receiver的等待队列 sendq waitq // sender的等待队列 // 互拆锁 lock mutex }

qcount代表chan 中已经接收但还没被取走的元素的个数,函数 len 可以返回这个字段的值;

dataqsiz和buf分别代表队列buffer的大小,cap函数可以返回这个字段的值以及队列buffer的指针,是一个定长的环形数组;

elemtype 和 elemsiz表示chan 中元素的类型和 元素的大小;

sendx:发送数据的指针在 buffer中的位置;

recvx:接收请求时的指针在 buffer 中的位置;

recvq和sendq分别表示等待接收数据的 goroutine 与等待发送数据的 goroutine;

sendq和recvq的类型是waitq的结构体:

type waitq struct { first *sudog last *sudog }

waitq里面连接的是一个sudog双向链表,保存的是等待的goroutine 。整个chan的图例大概是这样:

Group 40

下面看一下创建chan,我们通过汇编结果也可以查看到make(chan int)这句代码会调用到runtime的makechan函数中:

const ( maxAlign = 8 hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1)) ) func makechan(t *chantype, size int) *hchan { elem := t.elem // 略去检查代码 ... //计算需要分配的buf空间 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: // chan的size或者元素的size是0,不必创建buf c = (*hchan)(mallocgc(hchanSize, nil, true)) // Race detector c.buf = c.raceaddr() case elem.ptrdata == 0: // 元素不是指针,分配一块连续的内存给hchan数据结构和buf c = (*hchan)(mallocgc(hchanSize+mem, nil, true)) // 表示hchan后面在内存里紧跟着就是buf c.buf = add(unsafe.Pointer(c), hchanSize) default: // 元素包含指针,那么单独分配buf c = new(hchan) c.buf = mallocgc(mem, elem, true) } c.elemsize = uint16(elem.size) c.elemtype = elem c.dataqsiz = uint(size) return c }

首先我们可以看到计算hchanSize:

maxAlign = 8 hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1))

maxAlign是8,那么maxAlign-1的二进制就是111,然后和int(unsafe.Sizeof(hchan{}))取与就是取它的低三位,hchanSize就得到的是8的整数倍,做对齐使用。

这里switch有三种情况,第一种情况是缓冲区所需大小为 0,那么在为 hchan 分配内存时,只需要分配 sizeof(hchan) 大小的内存;

第二种情况是缓冲区所需大小不为 0,而且数据类型不包含指针,那么就分配连续的内存。注意的是,我们在创建channel的时候可以指定类型为指针类型:

//chan里存入的是int的指针 c := make(chan *int) //chan里存入的是int的值 c := make(chan int)

第三种情况是缓冲区所需大小不为 0,而且数据类型包含指针,那么就不使用add的方式让hchan和buf放在一起了,而是单独的为buf申请一块内存。

发送数据 channel的阻塞非阻塞

在看发送数据的代码之前,我们先看一下什么是channel的阻塞和非阻塞。

一般情况下,传入的参数都是 block=true,即阻塞调用,一个往 channel 中插入数据的 goroutine 会阻塞到插入成功为止。

非阻塞是只这种情况:

select { case c <- v: ... foo default: ... bar }

编译器会将其改为:

if selectnbsend(c, v) { ... foo } else { ... bar }

selectnbsend方法传入的block就是false:

func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) { return chansend(c, elem, false, getcallerpc()) } chansend方法

向通道发送数据我们通过汇编结果可以发现是在runtime 中通过 chansend 实现的,方法比较长下面我们分段来进行理解:

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { if c == nil { // 对于非阻塞的发送,直接返回 if !block { return false } // 对于阻塞的通道,将 goroutine 挂起 gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2) throw("unreachable") } ... }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpydgx.html