chan通道源码

118 阅读5分钟

chan通道源码

参考链接

环境

  • golang-1.17.8
  • 系统mac
  • 作图工具,在线processon
  • 源代码调试工具dlv

GOT

  • chan的底层数据结构
  • chan的工作流程
  • chansend和chanrev的交互流程。
  • 环形缓冲区的实现
  • 案例的分析学习

数据结构

chan结构.jpg
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
    elemtype *_type // element type
    sendx    uint   // send index
    recvx    uint   // receive index
    recvq    waitq  // list of recv waiters
    sendq    waitq  // list of send waiters

    lock mutex
}
  • qcount

相当于,底层buf的长度,len,即实际数据元素的个数。

//go:linkname reflectlite_chanlen internal/reflectlite.chanlen
func reflectlite_chanlen(c *hchan) int {
    if c == nil {
            return 0
    }
    return int(c.qcount)
}
  • dataqsiz

相当于,底层buf的容量

//go:linkname reflect_chancap reflect.chancap
func reflect_chancap(c *hchan) int {
    if c == nil {
            return 0
    }
    return int(c.dataqsiz)
}
  • buf

一个底层数组,使用sendx和recvx模拟了一个环形队列,重用已经使用过的空间,循环使用

  • elemesize

chan元素的大小

  • closed

判断chan是否已经关闭,非0则为关

  • elemtype

chan元素类型,主要是用来make(chan) 的时候初始化空间用的

  • sendx

写,当前操作要在buf中写入新元素的位置。每次有新值写入后,sendx++,越界判断

  • recvx

读,当前操作要读取buf中数据元素的位置。读取完后,recvx++,越界判断

  • recvq

接受 等待sudog队列,一个双向链表队列。

type waitq struct {
    first *sudog
    last  *sudog
}
  • sendq

发送等待sudog队列

  • lock

锁,读写操作的并发保护

例子

通过一个例子我们来了解一下。

func main() {
    ch := make(chan string, 2)
    go func() {
            time.Sleep(1 * time.Minute)
            fmt.Println("[go func]", <-ch) // 读出来时C
    }()
    ch <- "a"
    ch <- "b"
    ch <- "c" // gopark 执行之后,程序就无法走下一步了。
    fmt.Println("[main]", <-ch)
}
  • chan通道容量的2,
  • 启动goroutine,等待一分钟后,从通道里面读出数据
  • 往通道写入数据【a,b】
  • 写入【c】是通道阻塞
  • 一分钟后打印
[go func] a 
[main] b

代码分析


# 查看代码的汇编信息 ,或者dlv
go tool compile -N -l -S main.go

 runtime.chansend1(SB)
 runtime.chanrecv1(SB)

【发送端】chansend

  • 从【recvq】等待队列中去没有取出数据
  • c.qcount < c.dataqsiz buf没有满则,将数据【a,b】写入buf,
  • 此时,buf的,sendx=0,因为buf是环形缓冲区,则sendx==dataqsiz时,sendx=0。

数据a【写入数据-a】

数据b【写入数据-b】

  • 写入新数据【c】时,【recvq】等待队列中没有数据, buf也满了,
  • 则将该goroutine打包成sudog,放到【sendq】等待队列中,
  • 并gopark挂起该sudog,阻塞,等待接收端的goready唤醒,释放。

数据c【写入数据-c】

【接收端】chanrecv

  • 一分钟过后
  • 从【sendq】等待队列中取出了数据
  • 根据recvx重buf中取出数据,并将数据给返回变量,将sendq中取出的数据添加到buf[recvx]中,recvx++,sendx=recvx
    • 环形goready,发送端的等待 sudog
    • 【解锁】return
打印【go func a

读取数据a【读出数据a-写入数据c】

  • 从【sendq】等待队列中没有取出数据
  • 从buf中取出数据,【b】,则recvx++,又因为recvx==dataqsiz,则recvx=0,qcount=1,【解锁】return
打印【main b

读出数据b【读出数据b】

【环形buf】

recvx=0,sendx=1,

0】【1】
【c

chan操作

写入

runtime.chan/chan_send

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	if sg := c.recvq.dequeue(); sg != nil {
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}

	if c.qcount < c.dataqsiz {
    // qp := buf[sendx]
		qp := chanbuf(c, c.sendx)
		if raceenabled {
			racenotify(c, c.sendx, nil)
		}
		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
	}

  // 省略
	mysg := acquireSudog()
	c.sendq.enqueue(mysg)
	atomic.Store8(&gp.parkingOnChan, 1)
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
}
  • 前置校验
  • 【加锁】
  • 如果chan已经关闭,则painc【解锁】reutrn
  • 如果【recvq】 等待协程队列里有数据,则直接取出,将数据send给sudog,【解锁】return
  • 如果 【qcount < dataqsiz】,buf中有空闲的空间,则将,数据ep写到buf中 【解锁】 return
  • 非阻塞【!block】,select的操作的是非阻塞的,他是不会往,waitq中写入数据的【解锁】return
  • 将当前goroutine打包成sudog,添加到sendq等待队列中
  • 调用gopark方法挂起goroutine,阻塞等待channel,让出cpu使用权,等待唤醒(等待接收端的唤醒)
func main() {
	ch := make(chan string, 2)
	go func() {
		time.Sleep(1 * time.Minute)
		fmt.Println(<-ch)
	}()
	ch <- "a"
	ch <- "b"
	ch <- "c" // gopark 执行之后,是否程序就无法走下一步了。
	fmt.Println(<-ch)
}

// 当 ch<-c,是,这一行会阻塞,
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
  • 释放sudog资源。

读取

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	// 前置判断条件

	if sg := c.sendq.dequeue(); sg != nil {
		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true, true
	}

	if c.qcount > 0 {
		qp := chanbuf(c, c.recvx)
		if raceenabled {
			racenotify(c, c.recvx, nil)
		}
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp) 
		}
		typedmemclr(c.elemtype, qp)
		c.recvx++
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		c.qcount-- // buf的中元素数量减1
		unlock(&c.lock)
		return true, true
	}

	if !block {
		unlock(&c.lock)
		return false, false
	}

	// 代码省略
	mysg := acquireSudog()
	c.recvq.enqueue(mysg)
	atomic.Store8(&gp.parkingOnChan, 1)
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
	return true, success
}
  • 前置校验

  • 【加锁】

  • 如果【sendq】 等待协程队列里有数据则,(buf此时说明已经len=cap)

    • 先将sendq队列中数据取出,
    • 从buf中取出recvx下标数据,并赋给接受元素,然后将等待队列中取出数据并赋值给buf中的recvx位置,
    • goready,唤醒发送端正在阻塞的sudog。
    • 【解锁】返回
  • 如果 【qcount > 0】,表示buf中有元素,则取出元素,recvx++,qcount-- 【解锁】 return

  • 非阻塞【!block】,select的操作的是非阻塞的,他是不会往,waitq中写入数据的【解锁】return

  • 将当前goroutine打包成sudog,添加到recvq等待队列中

  • 调用gopark方法挂起goroutine,阻塞等待channel,让出cpu使用权,等待唤醒(等待发送端的环形)

环形缓冲区

环形队列是一种固定长度FIFO数据结构,数据移除,无需数据搬移,空间复用性高。

在这里指的是,hchan的buf,设置buf的大小,然后元素的一直在buf中进进出出。

写入

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
  // 环形buf中还有空间可用的空间
  if c.qcount < c.dataqsiz {
    qp := chanbuf(c, c.sendx) // qp := buf[c.sendx]
      if raceenabled {
        racenotify(c, c.sendx, nil)
      }
      typedmemmove(c.elemtype, qp, ep) // 将数据ep写入到qp中,
      c.sendx++
      if c.sendx == c.dataqsiz {
        c.sendx = 0
      }
      c.qcount++
      unlock(&c.lock)
      return true
    } 
}
  • recvq队列中没有阻塞的sudog,可能在buf中,buf中还可以可用空间则,则将要写入的元素,写到 buf[c.sendx]中,

  • sendx++,即下一次要写入的位置,sendx越界判断,则设置为0,收尾相连。

  • qcount++

读取

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
  	if sg := c.sendq.dequeue(); sg != nil {
		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true, true
	}
}

func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
  	qp := chanbuf(c, c.recvx)
		if raceenabled {
			racenotify(c, c.recvx, nil)
			racenotify(c, c.recvx, sg)
		}s
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp)
		}
		typedmemmove(c.elemtype, qp, sg.elem)
		c.recvx++
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
  • sendq队列中有数据则表示,buf中的数据是满的,
  • chansend的写入数据的优先级是【有recvq则直接发送 > 有空闲buf > 写入sendq】
  • 当recv被调用时,buf一定是满的。所以这里的操作的qcount是不会减少的,取出元素,并添加新的元素。
  • chanbuf(c,c.recvx) 取出要发送的元素,qp,
  • typedmemmove(c.elemtype, ep, qp) , buf中qp的值赋值给 ep
  • typedmemmove(c.elemtype, qp, sg.elem)sendq中的sudgo值,赋值给 buf[recvx]即 qp
  • recvx的下标操作

队列(双向链表)

chan中waitq的作用:

  • 档buf满了的时候,新增的请求可以暂时存放到等待队列中,等待下次消费和生产的双方取出队列中数据,
  • 当buf满了,还有大量新增的请求是,可以起到削峰作用,挂起当前gorontine,等待另一方goready,然后释放
type waitq struct {
    first *sudog
    last  *sudog
}

总结

  • 通过案例和图解分析,大致明白chan的处理流程。
  • chan通过,【等待队列】【环形buf】【lock】【gopark+goready】实现数据的,异步和同步传输
  • chansend和chanrecv的,处理数据的优先级
  • 明白chanrecv和chansend的从队列中取出的处理方式不同

Q&A

  • 代码里面的block的代表的是什么?代表是否是阻塞式调用

对chan的非select操作的block则是true,阻塞的,select操作则是false,也就是说的在select中操作,chan的读写是不会进入的waitq的。

  • chanrecv和chansend的从队列中取出的处理方式?
  • chanrecv从【sendq】队列中取出数据而不是里面接收这个数据,而是先取出【环形buf】中的数据返回,然后将队列中数据再设置到buf中

符合的通道特性,先来先出。

  • chansend从【recv】队列中取出数据后,则立马发送给接受者。

说明此时有大量的【recv】空闲者,则不必再放在buf中,优先先发给等待的recv者。

TODO

  • goready和gopark的交互,goready如何唤醒正在gopark的sudog呢?
  • sudog有是什么?
  • gmp的学习