golang channel底层剖析

golang channel底层剖析

文章目录

  • golang channel底层剖析
    • 内存模型
    • send和recv的流程
    • 多路select
    • channel源码阅读
      • 创建channel
      • 发送元素
      • 接收元素

内存模型

type hchan struct {
 qcount   uint           // queue 里面有效用户元素,这个字段是在元素出对,入队改变的;
 dataqsiz uint           // 初始化的时候赋值,之后不再改变,指明数组 buffer 的大小;
 buf      unsafe.Pointer // 指明 buffer 数组的地址,初始化赋值,之后不会再改变;
 elemsize uint16  // 指明元素的大小,和 dataqsiz 配合使用就能知道 buffer 内存块的大小了;
 closed   uint32
 elemtype *_type // 元素类型,初始化赋值;
 sendx    uint   // send index
 recvx    uint   // receive index
 recvq    waitq  // 等待 recv 响应的对象列表,抽象成 waiters
 sendq    waitq  // 等待 sedn 响应的对象列表,抽象成 waiters

 // 互斥资源的保护锁,官方特意说明,在持有本互斥锁的时候,绝对不要修改 Goroutine 的状态,不能很有可能在栈扩缩容的时候,出现死锁
 lock mutex
}

send和recv的流程

初始状态下,ch的缓冲区为空,读和写的下标都指向下标0的位置,等待队列也都为空。

golang channel底层剖析_第1张图片

然后一个协程g1向ch中发送数据,因为没有协程在等待接收数据,所有元素会被存入缓冲区中,sendx会从0开始向后挪,第五个元素会放到下标为的4的位置,然后sendx会重新回到0。
此时缓冲区中已经没有空闲的位置了。所以接下来发送的第6个元素无处可放,g1会进到ch的发送等待队列中。
golang channel底层剖析_第2张图片

发送等待队列是一个sudog类型的链表。

type sudog struct {
	g *g    // 记录哪个协程在等待
	isSelect bool
	next *sudog
	prev *sudog
	elem unsafe.Pointer  // 等待发送的信息在哪
	acquiretime int64
	ticket uint64
	parent *sudog
	waitlink *sudog
	waittail *sudog
	c *hchan   // 等待哪一个channel
}

里面会记录哪个协程在等待,等待哪一个channel,等待发送的信息在哪等信息。

接下来协程g2从ch接收一个元素,recvx指向下一个位置,第0个位置就空出来了。所以会唤醒sendq中的g1,将这里的数据发送给ch。然后缓冲区再次满了,sendq队列为空。

golang channel底层剖析_第3张图片

在这一过程中,可以看到sendxrecvx,都会从0到4再到0这样循环变化,所以channel的缓冲区被称为环形缓冲区

发送数据

所以像诸如 ch <- 10 这样给channel发送数据时,只有在缓冲区还有空闲位置,或者有协程在等着接收数据的时候才不会发生阻塞
碰到channel为nil,或者ch没有缓冲区,而且也没有协程等着接收数据,又或者ch有缓冲区但缓冲区已用尽的情况,都会发生阻塞

那如果不想阻塞的话,就可以使用select关键字。

select {
	case ch <- 10;
	...
	default:
	...
}

使用这种写法时,如果检测到ch可以发送数据,就会执行case分支;如果会阻塞,就会执行default分支了。

接收数据

接收数据的写法要更多一些。

// 将结果直接丢弃
<-ch
// 将结果赋值给变量v
v := ch
// common ok风格写法
// ok为false时表示ch已经关闭,此时v是channe元素类型的零值
v, ok := <-ch

以上几种写法都允许发生阻塞,只有在缓冲区中有数据,或者有协程等着发送数据时,才不会发生阻塞。如果channel为nil,或者channel无缓冲而且没有协程等着发送数据,又或者channel有缓冲但是缓冲区无数据时,都会发生阻塞。

如果无论如何都不想阻塞,t=同样可以采用select关键字。这样在检测到channel的recv操作不会阻塞时,就会执行case分支;如果会阻塞,就会执行default分支。

多路select

多路select指的是存在两个或更多的case分支,每个分支可以是一个channel的send或者recv操作。

例如一个协程通过多路select,等待ch1和ch2。这里default分支是可选的,我们暂且把这个协程记为g1。

var a, b int
b = 10
select {
	case a = <-ch1:
		println(v)
	case ch2 <- b:
	default:
}

多路select会被编译器转换为对runtime.selectgo函数调用。

func selectgo(cas0 *scase, order0 *unint16, pc0 *unintptr, nsends, nrecvs int, block bool) (int, bool)

第一个参数cas0指向一个数组,数组里面装的是select中所有的case分支。顺序是send在前recv在后。

golang channel底层剖析_第4张图片

第二个参数order0指向一个uint16类型的数组,数组大小等于case分支的两倍,实际上被用作两个数组。第一个数组用来对所有channel的轮询进行乱序。第二个数组用来对所有channel的加锁操作进行排序。因为轮询需要乱序才能保障公正性。而按照固定算法确定加锁顺序才能避免死锁。
在这里插入图片描述
第三个参数和race检测有关,我们暂且不关心。

剩下的nsends和nrecvs分别表示所有case中,执行send和recv操作的分支分别有多少个。
block表示多路select是否需要阻塞等待,对应到代码只能就是有default分支的不会阻塞,没有的会阻塞。

golang channel底层剖析_第5张图片

再来看第一个返回值,它代表最终哪个case分支被执行了。
第二个返回值用于在执行recv操作的case分支时,表明是实际接收到了一个值,还是因channel关闭而得到了零值。
多路select需要进行轮询来确定哪个case分支可操作了。但是轮询前要先加锁,所以selectgo函数在执行时,会先按照有序的加锁顺序,对所有的channel加锁,然后按照乱序的轮询顺序,检查所有channel的等待队列和缓冲区。

假如检查到ch1时,发现有数据可读,那就直接拷贝数据,进入对应分支;假如所有channel都不可操作,就把当前协程添加到所有channel的sendq或recvq中。

对应到这个例子,g1会被添加到ch1的recq,以及ch2的sendq中。之后g1会挂起,并解锁所有的channel。

假如接下来的ch1有数据可读了,g1就会被唤醒,完成对应的分支操作后,会再次按照加锁顺序对所有channel加锁,然后从所有sendq或recvq中将自己移除。最后全部解锁后返回。

golang channel底层剖析_第6张图片

channel源码阅读

创建channel

创建channel的源码在runtime/chan.go文件中的makechan函数,该函数在分配好内存之后,直接返回一个runtime.hchan类型的指针:

func makechan(t *chantype, size int) *hchan {
	elem := t.elem

	// 最多存65535个元素
	// 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")
	}
	// 查看需要的缓冲区是否越界
	// math.MulUintptr会将两数相乘,越界的话overflow是负数
	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.
		c = (*hchan)(mallocgc(hchanSize, nil, true))
		// Race detector uses this location for synchronization.
		c.buf = c.raceaddr()
	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:
		// Elements contain pointers.
		c = new(hchan)
		c.buf = mallocgc(mem, elem, true)
	}

	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
}

发送元素

注意: channel的整个发送过程和接收过程都使用了runtime.mutex进行加锁。runtime.mutex是runtime相关源码中常用的一个轻量级锁。

不管是有缓冲的channel还是无缓冲的channel,他们发送元素的源码都是一样的,都会调用chansend函数:

// ep是待发送数据的首地址
// block代表写入操作是否阻塞
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	// 如果channel为nil
	if c == nil {
		// 如果写入操作不阻塞,就返回false
		if !block {
			return false
		}
		// 永久阻塞
		gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}

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

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

	// 如果block非阻塞
	// 且channel没有关闭
	// 且(channel非缓冲队列且接收队列receiver为空)或者(channel为有缓冲队列且buf已满 )
	// 直接返回false
	if !block && c.closed == 0 && full(c) {
		return false
	}

	var t0 int64
	if blockprofilerate > 0 {
		t0 = cputicks()
	}

	// 获取同步锁,保证协程安全
	lock(&c.lock)

	// 如果通道已经关闭,写入数据产生panic
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}

	// 核心部分
	// 如果接收队列recvq不为空,即有goroutine在接收队列中等待时
	// 这里不用区分有缓冲和无缓冲channel
	// 跳过缓冲区,直接将数据发送给等待的接收者goroutine
	if sg := c.recvq.dequeue(); sg != nil {
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}

	// 如果接收队列recv为空
	// 且缓冲区数据大小 < 通道大小,说明此时发送队列为空
	if c.qcount < c.dataqsiz {
	
		// 直接将数据放入缓冲区
		qp := chanbuf(c, c.sendx)
		if raceenabled {
			racenotify(c, c.sendx, nil)
		}
		// 数据转移:本质上是内存拷贝,ep处拷贝到qp处
		typedmemmove(c.elemtype, qp, ep)
		c.sendx++
		if c.sendx == c.dataqsiz {
			c.sendx = 0
		}
		c.qcount++
		unlock(&c.lock)
		return true
	}

	// 如果以上条件都不满足
	// 即接收队列为空,且缓冲区buf数据量大小 == 通道大小
	// 如果block为非阻塞,解锁并返回false 
	if !block {
		unlock(&c.lock)
		return false
	}

	// 如果接收队列recv不为空
    // 且缓冲队列已满
    // 则将当前的goroutine加入到sendq队列
	gp := getg()
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
	mysg.elem = ep
	mysg.waitlink = nil
	mysg.g = gp
	mysg.isSelect = false
	mysg.c = c
	gp.waiting = mysg
	gp.param = nil
	// 将当前goroutine的sudog加入到sendq
	c.sendq.enqueue(mysg)
	// 将当前goroutine的sudog加入到sendq
	atomic.Store8(&gp.parkingOnChan, 1)
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
	KeepAlive(ep)

	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
	releaseSudog(mysg)
	if closed {
		if c.closed == 0 {
			throw("chansend: spurious wakeup")
		}
		panic(plainError("send on closed channel"))
	}
	return true
}

总结:

  1. 判断 channel 是否是nil,如果是,则会永久阻塞导致死锁报错。
  2. 如果 channel 中 recvq 存在接收者 goroutine,则直接把需要发送的数据拷贝到接收者 goroutine,recvq 储存的其实是一个以 sudog 结构为节点的链表,sudog结构中保存了接收者 goroutine 的指针。
  3. 如果 recvq 中不存在接收者:
    • 如果buf没有满,就直接把数据拷贝到 buf 的 sendx 位置。
    • 如果 channel 为无缓冲 channel 或 buf 已满,则把当前 goroutine 保存到 sendq 等待队列中,阻塞当前 goroutine。

接收元素

接收元素会调用chanrecv函数:

// block表示当channel无法返回数据的时候是否阻塞等待
// 比如当block为false并且channel中没有数据时,直接返回
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	if debugChan {
		print("chanrecv: chan=", c, "\n")
	}
	// 如果通道为空或未初始化
	if c == nil {
		// 如果block为非阻塞
        // 直接返回
		if !block {
			return
		}
		// 如果block为阻塞,调用gopark()阻塞当前goroutine
		gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}
	// 如果通道不为空或者已经初始化
    // 如果block为非阻塞接收
    // 且((通道为无缓冲通道且发送对列为空) 或者 (通道为有缓冲通道且缓冲区元素为0且通道未关闭))
    // 直接return false false
	if !block && empty(c) {
		if empty(c) {
			// The channel is irreversibly closed and empty.
			if raceenabled {
				raceacquire(c.raceaddr())
			}
			if ep != nil {
				typedmemclr(c.elemtype, ep)
			}
			return true, false
		}
	}

	var t0 int64
	if blockprofilerate > 0 {
		t0 = cputicks()
	}
	// 获取全局锁
	lock(&c.lock)

	// 核心原理
	
	// 如果通道已经关闭
	// 且缓冲区中无元素
	// 直接返回true和false(非正常返回)
	if c.closed != 0 && c.qcount == 0 {
		if raceenabled {
			raceacquire(c.raceaddr())
		}
		unlock(&c.lock)
		if ep != nil {
			// 返回空值
			// typedmemclr根据类型清理相应的地址的内存
			typedmemclr(c.elemtype, ep)
		}
		return true, false
	}

	// 如果等待发送队列sendq不为空(此时缓冲区一定是满的)
	// 两种情况:
	// 1.非缓冲型channel,直接进行内存拷贝
	// 2.缓冲型channel,但buf满了,则接收buf头部的元素,并将发送队列头goroutine的元素放到循环数组的尾部
	if sg := c.sendq.dequeue(); sg != nil {
		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true, true
	}

	// 如果等待发送队列sendq为空
    // 且通道非空  (注意: 一定是缓冲型channel)
    // 则从通道中获取数据
	if c.qcount > 0 {
		// 从循环数组里找到要接收的元素
		qp := chanbuf(c, c.recvx)
		if raceenabled {
			racenotify(c, c.recvx, nil)
		}
		// ep不为nil,说明未忽略要接收的值,即val<-ch,非<-ch
		if ep != nil {
			// 内存拷贝
			typedmemmove(c.elemtype, ep, qp)
		}
		// 清理循环数组recvx处的值
		typedmemclr(c.elemtype, qp)
		c.recvx++
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		c.qcount--
		unlock(&c.lock)
		return true, true
	}
	// 如果等待发送队列sendq为空
    // 且通道中元素个数为0,通道为空
    // 且block为非阻塞
    // 则直接返回false,false
	if !block {
		unlock(&c.lock)
		return false, false
	}

	// 如果等待发送队列sendq为空
    // 且通道中元素个数为0,通道为空
    // 且block为非阻塞
    // 则直接返回false,false
	gp := getg()
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
	// 保存待接收变量的地址ep
	mysg.elem = ep
	mysg.waitlink = nil
	gp.waiting = mysg
	mysg.g = gp
	mysg.isSelect = false
	mysg.c = c
	gp.param = nil
	// goroutine加入等待接收队列
	c.recvq.enqueue(mysg)

	atomic.Store8(&gp.parkingOnChan, 1)
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

	// 如果被唤醒了,继续执行扫尾工作
	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. 判断 channel 是否是nil,如果是,则会永久阻塞导致死锁报错。
  2. 如果 channel 中 sendq 有等待发送数据的 goroutine:
    • 如果是无缓冲 channel,则直接把要发送的数据拷贝到接受者的 goroutine 中,并唤醒发送方 goroutine。
    • 如果是有缓冲的 channel(说明此时缓冲区满了),则把 buf 中 recvx 位置的数据拷贝到 buf 中的 sendx 位置,并唤醒发送的 goroutine。
  3. 如果 channel 中 sendq 没有等待发送数据的 goroutine:
    • 如果 buf 有数据,则把 buf 中的 recvx 位置的数据拷贝到当前的接收goroutine。
    • 如果 buf 没有数据,则把当前 goroutine 加入 recvq 等待队列中,并挂起。

你可能感兴趣的:(go,golang)