Go源码--channel源码解读

简介

channel顾名思义就是channel的意思,主要用来在协程之间传递数据,所以是并发安全的。其实现原理,其实就是一个共享内存再加上锁,底层阻塞机制使用的是GMP模型。可见 GMP模型就是那个道,道生一,一生二,二生三,三生万物。

一个简单的例子 如下:

go 复制代码
func main() {
	c := make(chan int32, 1)

	go func() {
		c <- 1
	}()

	go func() {
		fmt.Println(<-c)
	}()

	time.Sleep(2 * time.Second)
	close(c)
}

运行结果是:

go 复制代码
1

其汇编部分代码如下:

go 复制代码
 CALL    runtime.makechan(SB)  // 对应 make(chan int32,1)
 ...
 CALL    runtime.chanrecv1(SB) // 对应 <-c 
 ...
 CALL    runtime.chansend1(SB) // 对应 c<-1 其中 <- 编译后就代表运行时 chanrecv1 函数 
 ...
 CALL    runtime.closechan(SB) // 对应 close(c)

CALL runtime.closechan(SB)

协程部分汇编代码,因为不是重点,略过 感兴趣的自己可以编译看看。

chan的所有内容都存放在runtime.hchan这个结构体中,makechan,chanrecv1和chansend1函数都是操作hchan这个结构体来实现chan的功能的。下面我们来看下 hchan结构体。

两种重要结构体

hchan 结构体

hchan结构体 如下

go 复制代码
type hchan struct {
	qcount   uint           // total data in the queue  环形队列里面的总的数据量 
	dataqsiz uint           // size of the circular queue // 环形队列大小 就是 make chan时 申请的大小
	buf      unsafe.Pointer // points to an array of dataqsiz elements // 指向环形队列的指针
	elemsize uint16         // 储存的元素类型占空间大小
	closed   uint32         // chan 状态 1 关闭 0 未关闭
	elemtype *_type         // element type // 元素类型   // make chan是 指定的类型 不过这个类型要进行运行时转换 但对应关系是这样的 
	sendx    uint           // send index // 发送索引
	recvx    uint           // receive index //  获取索引
	recvq    waitq          // list of recv waiters // 获取协程等待队列
	sendq    waitq          // list of send waiters // 发送携程等待队列

	lock mutex // 锁
}
waitq 结构体

waitq结构体用来存储阻塞在chan上的协程状态sudog结构体(内部包含了 协程信息)

其结构如下:

go 复制代码
// 等待队列
type waitq struct {
	first *sudog // 双向链表 头
	last  *sudog // 双向链表 尾
}

这个结构体有两个函数 enqueuq 和dequeue 插入链表和删除链表 就是双向链表的基础操作

hchan的结构丑图如下:

接下来我们按照 执行流程来梳理下部分源码 源码位置在 runtime/chan.go中,首先是 make(chan int32,1)函数,我们都知道 make函数可以初始化,map,slice和chan。编译时根据不同类型,会调用makechan,makeslcie和makemap函数。我们来看下 makechan函数。

makechan

其源码如下:

请结合比较丑流程图来看,比较好理解。

go 复制代码
func makechan(t *chantype, size int) *hchan {
	elem := t.Elem

	// compiler checks this but be safe.
	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"))
	}

	// Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
	// buf points into the same allocation, elemtype is persistent.
	// SudoG's are referenced from their owning thread so they can't be collected.
	// TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
	var c *hchan
	switch {
	case mem == 0:
		// Queue or element size is zero.
		// 如果chan是空的 则需要申请hchanSize空间 以满足 内存对齐要求
		c = (*hchan)(mallocgc(hchanSize, nil, true))
		// Race detector uses this location for synchronization.
		c.buf = c.raceaddr()
	case elem.PtrBytes == 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:
		// 分配内存
		// Elements contain pointers.
		c = new(hchan)
		c.buf = mallocgc(mem, elem, true)
	}
	// 初始化hchan
	c.elemsize = uint16(elem.Size_)
	c.elemtype = elem
	c.dataqsiz = uint(size)
	lockInit(&c.lock, lockRankHchan)

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

其实 就是 根据 chan 中元素类型和 大小 来计算需要的存储空间。逻辑还是比较清晰的。初始化了空间后,接下来就应该向chan存数据了,上例中是c <- 1,在编译时会转译成 chansend1函数。其源码如下:

go 复制代码
func chansend1(c *hchan, elem unsafe.Pointer) {
	chansend(c, elem, true, getcallerpc())
}

它只是调用了 chansend 函数 且 阻塞 状态是 true(阻塞状态true代表 如果 chan 的 buf满员,则写协程放入sendq,如果 chan 为空,则读协程会放入 recvq。 如果阻塞状态是 false 如果chan buf,满员 则返回错误。如果chan 为空,则读协程读取chan返回错误。阻塞状态为false 主要是select函数用)。接下来我们来看下其源码:

chansend

go 复制代码
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	// 省略部分代码

	// chan没关闭 且 缓存buf已满 且 是非阻塞模式(select方法使用)  则不会写入 sendq里 直接返回错误
	if !block && c.closed == 0 && full(c) {
		return false
	}
	// todo
	var t0 int64
	if blockprofilerate > 0 {
		t0 = cputicks()
	}
	// 加锁
	lock(&c.lock)
	
	// 如果chan已经关闭 直接panic
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}

	// 有阻塞的取协程 送的数据 直接交给取协程 并将取协程唤醒
	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).
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}

	// 如果没有阻塞的获取协程 且 channel容量没满 则需要插入channel中
	if c.qcount < c.dataqsiz {
		// 获取要放的位置的指针
		qp := chanbuf(c, c.sendx)
		if raceenabled {
			racenotify(c, c.sendx, nil)
		}
		// 将元素 放入其中
		typedmemmove(c.elemtype, qp, ep)
		c.sendx++
		// ring buffer 循环数组 到尾 就 从头开始
		if c.sendx == c.dataqsiz {
			c.sendx = 0
		}
		c.qcount++
		// 解锁
		unlock(&c.lock)
		return true
	}
	// buf满了 但是没有 阻塞 则直接返回失败
	if !block {
		unlock(&c.lock)
		return false
	}

	// 如果channel满了 则所有send协程就需要加入 sendq 里排队,创建一个 等待状态的 sudog 包装当前 协程和数据 ep 放入 sendq中
	// Block on the channel. Some receiver will complete our operation for us.
	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.
	// 初始化 sudog
	mysg.elem = ep
	mysg.waitlink = nil
	mysg.g = gp
	mysg.isSelect = false
	mysg.c = c
	gp.waiting = mysg
	gp.param = nil
	// 放入 发送等待队列
	c.sendq.enqueue(mysg)
	// Signal to anyone trying to shrink our stack that we're about
	// to park on a channel. The window between when this G's status
	// changes and when we set gp.activeStackChans is not safe for
	// stack shrinking.
	gp.parkingOnChan.Store(true)
	// 调用GMP模型 阻塞协程 并释放 chan的 lock 锁,释放后别的协程可以进来
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceBlockChanSend, 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.
	// ep保持活着 不被垃圾回收
	KeepAlive(ep)

	// 从等待队列唤醒 
	// someone woke us up.
	if mysg != gp.waiting {
		throw("G waiting list is corrupted")
	}

	gp.waiting = nil
	gp.activeStackChans = false
	closed := !mysg.success
	gp.param = nil
	if mysg.releasetime > 0 {
		blockevent(mysg.releasetime-t0, 2)
	}
	mysg.c = nil
	// 释放sudog
	releaseSudog(mysg)
	if closed {
		if c.closed == 0 {
			throw("chansend: spurious wakeup")
		}
		panic(plainError("send on closed channel"))
	}
	return true
}

其执行主流程如下:

首先加锁

  1. 如果recvq里有数据,则将recvq链表头节点中的sudog拿出来,将send的数据直接交给sudog的recv协程,然后唤醒这个协程,最后释放当前send协程拥有的锁,返回。
  2. 否则 如果 hchan buf没满,则将数据存入其中,更新 qcount的值,释放锁,返回。
  3. 否则 初始化一个sudog并将 当前 send协程放入其中,并阻塞(调用GMP模型),然后释放当前send协程拥有的锁(别的send协程可以执行 chansend)
  4. 当前阻塞的send协程被待被recv协程唤醒。
  5. 唤醒后 将 当前协程状态变为非等待,释放当前协程对应的sudog 返回

这里有注意的点,sendq里的协程只能被 recv协程唤醒,反之亦然。这里就带来一个有趣的问题,sendq和recvq能都有数据吗?大神们可以思考下。

send讲完了,接下来改recv了,要不不就阻塞了吗,

例子中的 < - c 是 编译时会转译成 chanrecv1,我们来看下源码:

go 复制代码
func chanrecv1(c *hchan, elem unsafe.Pointer) {
	chanrecv(c, elem, true)
}

其block参数也是 true ,这阻塞跟 send一样。我们来看下 chanrecv方法吧

chanrecv

其源码如下:

go 复制代码
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	// 非主逻辑代码 跳过

	lock(&c.lock)

	// 如果chan已关闭 且qcount==0 则 证明chan中已没有数据 返回
	if c.closed != 0 {
		if c.qcount == 0 {
			if raceenabled {
				raceacquire(c.raceaddr())
			}
			unlock(&c.lock)
			if ep != nil {
				typedmemclr(c.elemtype, ep)
			}
			return true, false
		}
		// The channel has been closed, but the channel's buffer have data.
	} else {
		// 如果chan没关闭 先看 sendq里 有没有阻塞获取sudog  如果有 取出来 直接将 数据给其中的协程 并唤醒 阻塞的读操作 并解锁
		// Just found waiting sender with not closed.
		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
		}
	}

	// 走到这里 证明 chan没关闭 且 sendq没数据 则从缓存区取数据
	if c.qcount > 0 {
		// Receive directly from queue
		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--
		unlock(&c.lock)
		return true, true
	}

	// 非阻塞(waitq不能有数据), 返回
	if !block {
		unlock(&c.lock)
		return false, false
	}

	// 将 协程 构造sudog 存放到 recvq 中 阻塞协程, 下面代码跟 sendq类似 
	// no sender available: block on this channel.
	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
	// 插入 recvq 链表尾部
	c.recvq.enqueue(mysg)
	// Signal to anyone trying to shrink our stack that we're about
	// to park on a channel. The window between when this G's status
	// changes and when we set gp.activeStackChans is not safe for
	// stack shrinking.
	gp.parkingOnChan.Store(true)
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceBlockChanRecv, 2)

	// someone woke us up
	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)
	}
	success := mysg.success
	gp.param = nil
	mysg.c = nil
	releaseSudog(mysg)
	return true, success
}

其执行主流程如下:

首先加锁

  1. 如果协程已关闭 且 buf中没有数据 则 返回
  2. 如果协程没关闭 则从 sendq里取sudog数据,如果取到了 就 将 数据 传递给 sudog的send协程 当前recv协程释放锁,唤醒send协程 返回。
  3. 如果 没有从sendq里取到数据,则从 buf 里取数据,更新qcount 然后 解锁,返回
  4. 如果 buf 为空 ,则初始化一个sudog 将 当前协程放入其中,阻塞当前recv协程,释放当前recv协程拥有的锁。
  5. 等待其他send协程唤醒 当前recv协程。
  6. 唤醒后 将 当前协程状态变为非等待,释放当前协程对应的sudog 返回

send和recv后,接下来就该 close了

close( c)编译后 函数 closechan函数 我们来看下

closechan

源码如下:

go 复制代码
func closechan(c *hchan) {
	if c == nil {
		panic(plainError("close of nil channel"))
	}
	
	// 加锁
	lock(&c.lock)
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("close of closed channel"))
	}

	if raceenabled {
		callerpc := getcallerpc()
		racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan))
		racerelease(c.raceaddr())
	}
 	
	// 将锁状态变为1 
	c.closed = 1

	var glist gList
	
	// 释放所有 读协程 
	// release all readers
	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 = unsafe.Pointer(sg)
		sg.success = false
		if raceenabled {
			raceacquireg(gp, c.raceaddr())
		}
		glist.push(gp)
	}
	
	// 释放所有写协程 包括其数据 所以 我们从关闭的协程里读的数据 就是 buf 中的 不会有 sendq里的数据
	// release all writers (they will panic)
	for {
		sg := c.sendq.dequeue()
		if sg == nil {
			break
		}
		sg.elem = nil
		if sg.releasetime != 0 {
			sg.releasetime = cputicks()
		}
		gp := sg.g
		gp.param = unsafe.Pointer(sg)
		sg.success = false
		if raceenabled {
			raceacquireg(gp, c.raceaddr())
		}
		glist.push(gp)
	}
	unlock(&c.lock)
	
	// 将所有协程唤醒 
	// Ready all Gs now that we've dropped the channel lock.
	for !glist.empty() {
		gp := glist.pop()
		gp.schedlink = 0
		goready(gp, 3)
	}
}

close 主要是将阻塞的 其读和写协程 携带的 元素 释放,(但是缓存buf里的数据并没释放)可以使得GC捕获,然后将阻塞的协程唤醒。这时 在 chansend函数 lock()处 阻塞的协程会panic,被goready 唤醒的协程会正常退出;在 chanrecv 函数 lock()处 阻塞的协程(或者继续执行<-c的协程)会继续从 buf 拿数据,当数据获取完后,会退出。

总结

channel 其实就是用了共享内存加锁这种机制来处理协程之间的共享数据的,这次阅读源码还是有些细节没整明白,整体理解的也不够透彻,还望大神指正。

相关推荐
在下不上天3 分钟前
Flume日志采集系统的部署,实现flume负载均衡,flume故障恢复
大数据·开发语言·python
陌小呆^O^16 分钟前
Cmakelist.txt之win-c-udp-client
c语言·开发语言·udp
ifanatic19 分钟前
[面试]-golang基础面试题总结
面试·职场和发展·golang
懒是一种态度27 分钟前
Golang 调用 mongodb 的函数
数据库·mongodb·golang
I_Am_Me_32 分钟前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
Iced_Sheep35 分钟前
干掉 if else 之策略模式
后端·设计模式
重生之我是数学王子42 分钟前
QT基础 编码问题 定时器 事件 绘图事件 keyPressEvent QT5.12.3环境 C++实现
开发语言·c++·qt
Ai 编码助手44 分钟前
使用php和Xunsearch提升音乐网站的歌曲搜索效果
开发语言·php
学习前端的小z1 小时前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
神仙别闹1 小时前
基于C#和Sql Server 2008实现的(WinForm)订单生成系统
开发语言·c#