Go channel源码解读

总结

  1. channel通过使用mutex实现线程安全,这点感觉比较重,不知道可否借鉴CurrentHashMap的分段锁思想优化,这样做的话,应该需要改变buf的实现,不能是循环队列了,因为CurrentHashMap是通过hashcode%length查找元素,channel是通过两个索引。
  2. ch := make(chan int)返回的是一个指针,所以我们在函数之间传递不需要用&ch
  3. goroutine 之间传递数据是通过共享内存实现的。精细的说是buf
  4. 在向一个无缓冲有接收/发送的gouroutine发送数据/接收数据的时候,是直接把该数据从该gotoutine的地址复制到另外一个gotoutine内存地址中,不是通过buf(无缓冲buf=0),这样就不用加锁/释放锁了。

channel 结构体

下面是channel 的源码

type hchan struct {
	qcount   uint           // 队列中当前数据个数=len(chan)
	dataqsiz uint           // channel的大小=cap(chan)
	//如果 qcount 和 dataqsiz 的值相同,则表示缓冲区用完了。
	
	buf      unsafe.Pointer // 指向存放数据的环形数组
	elemsize uint16 //channel 中数据类型的大小 (单个元素的大小)
	closed   uint32 // 表示当前通道是否处于关闭状态。创建通道后,该字段设置为0,即通道打开; 通过调用close将其设置为1,通道关闭。
	elemtype *_type //  队列中的元素类型
	sendx    uint   // 当前发送元素的索引
	recvx    uint   // 当前接收元素的索引
	recvq    waitq  // 接收等待队列;如果缓冲区没有数据而且当前没有别的发送者在等待,那么执行者 goroutine 会被挂起,并且将对应的 sudog 对象放到 recvq 中。
	sendq    waitq  // 发送等待队列;一个发送语句执行时,如果缓冲区已经满了,而且没有接收者在等待,那么执行者 goroutine 会被挂起,并且将对应的 sudog 放到 sendq 中。
	
	//同步锁 可以保护改结构体中的所有字段
	lock mutex
}
//一个双向链表
type waitq struct {
	first *sudog
	last  *sudog
}

hchan 中大概分为三类属性

  1. waitq 相关属性,比如 recvq sendq,可以理解为一个 FIFO 队列, 是一个双向链表
  2. buf 相关属性,如dataqsiz、qcount ,当channel设置缓冲区>0时有意义
  3. 其他标志属性 lock closed等

使用data := make(chan int, 3)后的状况:

hchan struct {
    qcount uint : 0 
    dataqsiz uint : 3 
    buf unsafe.Pointer : 0xc00007e0e0 
    elemsize uint16 : 8 
    closed uint32 : 0 
    elemtype *runtime._type : &{
        size:8 
        ptrdata:0 
        hash:4149441018 
        tflag:7 
        align:8 
        fieldalign:8 
        kind:130 
        alg:0x55cdf0 
        gcdata:0x4d61b4 
        str:1055 
        ptrToThis:45152
        }
    sendx uint : 0 
    recvx uint : 0 
    recvq runtime.waitq : 
        {first:<nil> last:<nil>}
    sendq runtime.waitq : 
        {first:<nil> last:<nil>}
    lock runtime.mutex : 
        {key:0}
}

goroutine的封装

下面的是对M-P-G 中G的封装

type sudog struct {

	g *g //正在等候的 goroutine

	// isSelect indicates g is participating in a select, so
	// g.selectDone must be CAS'd to win the wake-up race.
	isSelect bool //
	next     *sudog
	prev     *sudog
	elem     unsafe.Pointer // data element (may point to stack)

	// The following fields are never accessed concurrently.
	// For channels, waitlink is only accessed by g.
	// For semaphores, all fields (including the ones above)
	// are only accessed when holding a semaRoot lock.

	acquiretime int64 //申请 sudog 对象 时间
	releasetime int64 //释放 sudog 对象 时间
	ticket      uint32
	parent      *sudog // semaRoot binary tree
	waitlink    *sudog // g.waiting list or semaRoot
	waittail    *sudog // semaRoot
	c           *hchan // channel
}

初始化channel

ch := make(chan int)

这个创建channel 背后使用的就是makechan这个函数,返回值就是一个*hchan ,一个channel指针,所以我们在各个函数传递的时候不需要传递channel指针。

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")
	}
	//memo = 缓冲队列元素个数 * 元素大小,overflow为是否溢出
	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 {
	//如果是unbuffered 那么初始化一个hchanSize大小的channel
	case mem == 0:
		// Queue or element size is zero.
		//当创建的是非缓冲通道
        //或者缓冲通道的元素类型大小为0(如 struct{}{})
        //只需要申请hchan的内存而不需要申请缓冲队列的内存
		c = (*hchan)(mallocgc(hchanSize, nil, true))
		// Race detector uses this location for synchronization.
		//c.buf直接指向申请的hchan的内存地址
		c.buf = c.raceaddr()
		
	//当创建的是缓冲通道,并且通道元素类型不是指针类型的
    //需要申请hchan的内存和缓冲队列的内存(hchanSize+mem)
	case elem.ptrdata == 0:
		// Elements do not contain pointers.
		// Allocate hchan and buf in one call.
		c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
		c.buf = add(unsafe.Pointer(c), hchanSize)
	default:
		//当创建的是缓冲通道,并且通道元素类型是指针类型
        //调用了两次mallocgc来申请内存,hchan和缓冲队列不共用内存(内存空间是不连续的)
		// Elements contain pointers.
		c = new(hchan)
		c.buf = mallocgc(mem, elem, true)
	}
	//初始化单个元素的类型大小 类型 channel容量
	c.elemsize = uint16(elem.size)
	c.elemtype = elem
	c.dataqsiz = uint(size)

	if debugChan {
		print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
	}
	return c
}

创建channel文字总结:

  1. 检查传入元素类型大小
  2. 检查buf申请的大小是否异常,检查传入的值是否大于0
  3. 当创建的是非缓冲通道或者缓冲通道的元素类型大小为0时,是不需要申请缓冲队列的内存的
  4. 当创建的是缓冲通道,并且通道元素类型不是指针类型的,会向系统申请一块连续内存,用来存放hchan结构体和缓冲队列,共用一个地址
  5. 当创建的是缓冲通道,并且通道元素类型是指针类型的,会向系统申请两块内存,用来存放hchan结构体和缓冲队列
  6. 初始化其他属性,如队列大小等

创建channel流程图

待画

channel 发送数据

对应的函数为:runtime.chansend1(ch, &v)

func chansend1(c *hchan, elem unsafe.Pointer) {
	chansend(c, elem, true, getcallerpc())
}

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	//1. 判断向其发送的channel是否为nil
	if c == nil {
		if !block {
			return false
		}
		// gopark函数将当前goroutine置于等待状态并通过unlockf唤醒
        // 但是传入的unlockf为nil(第一个参数)
        // 2. 这里就是向一个未初始化的通道发送数据,会永久阻塞!!
		gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}

	if debugChan {
		print("chansend: chan=", c, "\n")
	}

	if raceenabled {
		racereadpc(c.raceaddr(), callerpc, funcPC(chansend))
	}

	//3. 非阻塞写 通道关闭 (buf为空且无接收队列 || 是缓冲队列但已经满了)
	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()
	}
	//4. 可以发送数据了,发送前先锁住
	lock(&c.lock)
	
	//5. 如果通道已经关闭,那么直接解锁然后panic
	//这里就是向一个已经关闭的channel发送数据会panic
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}

	//6. 判断等待接收队列中是否有goroutine,有的化取出头部的值
	if sg := c.recvq.dequeue(); sg != nil {
		// Found a waiting receiver. We pass the value we want to send
		// directly to the receiver, bypassing the channel buffer (if any).
		//当有 goroutine 在 recvq 队列上等待时,跳过缓存队列,将消息直接发给 reciever goroutine;从等待接受的 goroutine 队列链表获取一个sudog,挂在P的本地执行队列中,goready 唤醒阻塞的 goroutine
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}
	
	//  c.qcount > 0 implies that c.recvq is empty.
	//  c.qcount < c.dataqsiz implies that c.sendq is empty.
	//7. 发送端队列为空,表示缓冲队列不满,可以发送数据
	if c.qcount < c.dataqsiz {
		// Space is available in the channel buffer. Enqueue the element to send.
		qp := chanbuf(c, c.sendx)
		if raceenabled {
			raceacquire(qp)
			racerelease(qp)
		}
		typedmemmove(c.elemtype, qp, ep)
		//索引移动
		c.sendx++
		//10.由于是环形队列,判断元素满了后重置索引为0
		if c.sendx == c.dataqsiz {
			c.sendx = 0
		}
		//buf数量加一
		c.qcount++
		//解锁
		unlock(&c.lock)
		return true
	}
	
	//阻塞 解锁直接返回 false
	if !block {
		unlock(&c.lock)
		return false
	}

	// Block on the channel. Some receiver will complete our operation for us.
	 //9. 剩余最后一种case了那就是 缓冲队列已经满了
	 //chan队列已满,阻塞 将本协程放入send队列中,同时休眠此协程
	//10.返回当前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.
	//11. 下面都是封装一个sudog
	//把发送的数据ep封装在sudog中
	mysg.elem = ep
	mysg.waitlink = nil
	//把goroutine封装在sudog中
	mysg.g = gp
	mysg.isSelect = false
	mysg.c = c
	gp.waiting = mysg
	gp.param = nil
	//12. 把这个封装好的sudog放到发送channel的队列中
	c.sendq.enqueue(mysg)
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
	// Ensure the value being sent is kept alive until the
	// receiver copies it out. The sudog has a pointer to the
	// stack object, but sudogs aren't considered as roots of the
	// stack tracer.
	//保证数据不被回收
	KeepAlive(ep)

	// someone woke us up.
	if mysg != gp.waiting {
		throw("G waiting list is corrupted")
	}
	gp.waiting = nil
	gp.activeStackChans = false
	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
}

发送数据文字总结:

  1. 首先判断发送的channel是否是nil
  2. 非阻塞写 通道关闭 (buf为空且无接收队列 || 是缓冲队列但已经满了)
  3. 可以发送数据了,发送前先上锁
  4. 如果通道已经关闭,那么直接解锁然后panic
    通道可以发送数据有三种case
  5. case1 :判断等待接收队列中是否有goroutine,当有 goroutine 在 recvq 队列上等待时,跳过buf,将消息数据复制给 recv goroutine;解锁;
  6. case2: 发送端队列为空,表示缓冲队列不满,数据放到buf中,调整sendx下标,buf++,解锁
  7. case3 : 缓冲队列已经满了,将该goroutine与数据用sudog封装一下,放到sendq中。然后挂起该goroutine
  8. 等待被唤醒。唤醒后如果不是当前goroutine那么直接panic
  9. 是的话释放该sudog;

发送数据流程图:

待画

channel接收数据

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	// raceenabled: don't need to check ep, as it is always on the stack
	// or is new memory allocated by reflect.

	if debugChan {
		print("chanrecv: chan=", c, "\n")
	}

	//如果channel为空则永久阻塞,因为唤醒不到它
	if c == nil {
		if !block {
			return
		}
		gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}
	//1.非阻塞读 2.无缓冲channel并且消费者环形队列头结点为空 或 有缓冲channel中存储的元素数量为0  3.没有关闭channel 直接返回
	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 关闭 且 缓冲元素为0 返回空值
	if c.closed != 0 && c.qcount == 0 {
		if raceenabled {
			raceacquire(c.raceaddr())
		}
		unlock(&c.lock)
		if ep != nil {
			typedmemclr(c.elemtype, ep)
		}
		return true, false
	}

	// 等待发送队列里有 goroutine 存在,说明 buf 是满的
	// 这有可能是:
	// 1. 非缓冲型的 channel
	// 2. 缓冲型的 channel,但 buf 满了
	// 针对 1,直接进行内存拷贝(从 sender goroutine -> receiver goroutine)
	// 针对 2,接收到循环数组头部的元素,并将发送者的元素放到循环数组尾部
	if sg := c.sendq.dequeue(); sg != nil {
		// Found a waiting sender. If buffer is size 0, receive value
		// directly from sender. Otherwise, receive from head of queue
		// and add sender's value to the tail of the queue (both map to
		// the same buffer slot because the queue is full).
		//直接把发送者的数据拷贝过来,然后解锁 返回
		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true, true
	}
	
	//缓冲型的
	if c.qcount > 0 {
		// Receive directly from queue
		//从缓冲区头部这个位置取出数据
		qp := chanbuf(c, c.recvx)
		if raceenabled {
			raceacquire(qp)
			racerelease(qp)
		}
		if ep != nil {
			//把刚刚取出的数据放到接收的goroutine中
			typedmemmove(c.elemtype, ep, qp)
		}
		//清理掉循环数组里相应位置的值
		typedmemclr(c.elemtype, qp)
		//下标移动一位
		c.recvx++
		//如果到头,从0开始(数组实现的循环队列)
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		//buf元素总数 -1
		c.qcount--
		//释放锁
		unlock(&c.lock)
		return true, true
	}
	// 非阻塞接收,解锁。selected 返回 false,因为没有接收到值
	if !block {
		unlock(&c.lock)
		return false, false
	}

	// no sender available: block on this channel.
	//buf没数据,也没有goroutine等待写,那么只能把自己挂起等待唤醒
	//下面的都是封装一个sndog的过程
	gp := getg()
	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
	gp.waiting = mysg
	mysg.g = gp
	mysg.isSelect = false
	mysg.c = c
	gp.param = nil
	//封装完毕,放到接收队列中
	c.recvq.enqueue(mysg)
	//挂起当前goroutine,等待被唤醒
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

	// someone woke us up
	//唤醒后开始清理该sudog
	if mysg != gp.waiting {
		throw("G waiting list is corrupted")
	}
	gp.waiting = nil
	gp.activeStackChans = false
	if mysg.releasetime > 0 {
		blockevent(mysg.releasetime-t0, 2)
	}
	closed := gp.param == nil
	gp.param = nil
	mysg.c = nil
	releaseSudog(mysg)
	return true, !closed
}

接收数据文字总结:

  1. 边界判断,主要是channel是否为nil,是的话,永久阻塞
  2. 当channel 没准备好接收:
    非缓冲型,等待发送列队里没有 goroutine 在等待
    缓冲型,但 buf 里没有元素

之后,又观察到 closed == 0,即 channel 未关闭。
因为 channel 不可能被重复打开,所以前一个观测的时候, channel 也是未关闭的,因此在这种情况下可以直接宣布接收失败,快速返回。因为没被选中,也没接收到数据,所以返回值为 (false, false)。
3. 上锁。粒度比较大。如果 channel 已关闭,并且循环数组 buf 里没有元素。对应非缓冲型关闭和缓冲型关闭但 buf 无元素的情况,返回对应类型的零值,但 received 标识是 false,告诉调用者此 channel 已关闭,你取出来的值并不是正常由发送者发送过来的数据。但是如果处于 select 语境下,这种情况是被选中了的。很多将 channel 用作通知信号的场景就是命中了这里。
4. 如果有等待发送的队列,说明 channel 已经满了,要么是非缓冲型的 channel,要么是缓冲型的 channel,但 buf 满了。这两种情况下都可以正常接收数据。
5. 如果是非缓冲型的,就直接从发送者的栈拷贝到接收者的栈。
6. 如果是缓冲型,而 buf 又满了的情形。先找到接收的位置,将该处的元素拷贝到接收goroutine。然后将发送者待发送的数据拷贝到接收的位置。这样就完成了接收数据和发送数据的操作。接着,分别将发送游标和接收游标向前进一,如果达到数组长度了,再从 0 开始。
7. 到这就是该阻塞了,如果 block 传进来的值是 false,那就不阻塞,直接返回就好了。
8. 先构造一个 sudog,接着就是保存各种值。这里会将接收数据的地址存储到了 elem 字段,当被唤醒时,接收到的数据就会保存到这个字段指向的地址。然后将 sudog 添加到 channel 的 recvq 队列里。调用 goparkunlock 函数将 goroutine 挂起。

接收数据流程图:

待画

关闭channel

未完待续

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值