Golang源码探究 —— chan

20 篇文章 4 订阅
7 篇文章 2 订阅


        在Golang中,chan是我们常用的数据结构,chan是并发安全的,在使用它的时候我们无需考虑锁的问题。而且chan使用非常方便,chan与goroutine配合,很容易就可以实现一个生产者/消费者模型。了解chan的源码可以让我们了解更多关于chan的实现以及更好的使用。

        在介绍chan之前,我们可以想一下chan的基本结构,首先,从chan的使用上我们应该就会猜到它里面应该是一个队列的结构,并且他采取了一定的措施来保证并发安全。其次,在读取或写入chan的时候,协程可能会阻塞,因此里面也需要有阻塞队列来保存阻塞的goroutine的g结构。
 

1、chan数据结构的定义

​ chan的实现在runtime包的chan.go中,chan的结构体定义如下:

type hchan struct {
	qcount   uint           // 队列中的总数据数量,也就是队列中当前存放的元素的个数
	dataqsiz uint           // 循环队列的大小,chan的大小
	buf      unsafe.Pointer // 指向数组的指针,逻辑上组成一个环形队列
	elemsize uint16         // 存放的元素类型的大小,比如int是8个字节
	closed   uint32         // chan是否被关闭的标志
	elemtype *_type         // 元素类型
	sendx    uint           // send index   发送的索引
	recvx    uint           // receive index  接收的索引
	recvq    waitq          // list of recv waiters  等待接收数据的goroutine阻塞队列
	sendq    waitq          // list of send waiters  等待发送数据的goroutine阻塞队列

    // lock用来保护hchan中的所有字段
	lock mutex
}

// sudog是对g的一个包装,其中包含了当前g,以及next和prev指针,可以组成一个双向链表
type sudog struct {
	g *g

	next *sudog
	prev *sudog
	elem unsafe.Pointer   // 可以使用这个指针来直接在g之间传递数据

	...   // 省略了不太关注的字段
}

// 等待队列,waitq 是 sudog 的一个双向链表
type waitq struct {
	first *sudog
	last  *sudog
}

有缓冲的chan int32 的结构如下图所示:

为了方便画图,将sendx放在了recvx的下面。可以看到chan中是有一个互斥锁的,chan并不是无锁队列。那么为什么它的效率还比较高呢,是因为只在传递数据时进行加锁,锁的粒度小,因此效率比较高。

在这里插入图片描述

2、创建chan

我们可以通过将代码生成汇编来查看创建chan底层调用的函数。将下面代码生成汇编代码可以看到在底层创建chan是调用了runtime.makechan函数。

package main

import "fmt"

func main() {
	channel := make(chan int, 5)
    // 为了不报错,打印一下
	fmt.Println(channel)
}

可以使用 go tool compile -N -l -S main.go 或 go build -gcflags -S main.go来将源代码编译为汇编代码。

在这里插入图片描述

makechan的代码如下:

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")
	}
	
    // 判断要申请的空间大小是否越界, mem为相乘后的结果,overflow为是否越界,bool类型
	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: 
        // make(chan type) 创建无缓冲的chan,此处为hchan结构分配空间

		c = (*hchan)(mallocgc(hchanSize, nil, true))
		...  
	case elem.ptrdata == 0:
        // 如果元素不包含指针,一次同时申请hchan和hchan.buf的内存
		c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
		c.buf = add(unsafe.Pointer(c), hchanSize)
	default:
        // 元素包含指针,分别申请内存
		c = new(hchan)
		c.buf = mallocgc(mem, elem, true)
	}
	
    // 记录元素大小
	c.elemsize = uint16(elem.size)
	// 记录元素类型
    c.elemtype = elem
    // 循环队列的大小,也就是可容纳的元素的个数,chan的大小
	c.dataqsiz = uint(size)
    // 初始化互斥锁
	lockInit(&c.lock, lockRankHchan)

	...
	return c
}

在makechan函数中,主要是对hchan结构进行赋值,并返回该结构体指针。

3、发送数据 c <-

向管道中发送数据,通过汇编看到调用了runtime.chansend1函数:

在这里插入图片描述

chan发送数据有三种情形:

  • 直接发送:发送时,环形队列中没有数据,而且有接收者正在读数据,直接将数据交给接收者。
  • 放入缓冲区:有缓冲的chan,而且缓冲区有空闲空间,直接将数据放入缓冲区中。
  • 休眠等待:无缓冲的chan或者缓冲区满了,而且没有接收者正在接受数据,发送者休眠等待。

 

3.1 直接发送

直接发送的情形如下,此时环形队列中没有数据,或者是无缓冲的chan。有goroutine正在接受队列中,出队一个G,然后直接将数据交给出队的G,然后将G唤醒,数据不会经过缓冲区。直接通过sudog中的elem进行数据传递(elem指针指向的是用户接受变量的地址,将数据拷贝到elem指向的地址,即可接收数据,比如对于 d <- c 操作,elem指向的就是d的地址)。

在这里插入图片描述

代码如下:

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    ... 
    
	// 出队一个g,如果sg != nil,说明,环形缓冲区中肯定没有数据(不然也不会有g在接收队列中了),或者是无缓冲的chan
	if sg := c.recvq.dequeue(); sg != nil {
		// 直接将数据传递给出队的g
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}

	...
}

func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
	...
    // 直接将数据传递过去
	if sg.elem != nil {
		sendDirect(c.elemtype, sg, ep)
		sg.elem = nil
	}
	gp := sg.g
	unlockf()
	gp.param = unsafe.Pointer(sg)
	sg.success = true
	if sg.releasetime != 0 {
		sg.releasetime = cputicks()
	}
    
    // 唤醒出队的协程
	goready(gp, skip+1)
}

func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
	...
    // 使用memmove将数据拷贝过去
	memmove(dst, src, t.size)
}

 

3.2 放入缓冲区

放入缓冲区的情形如下:此时没有正在接收的goroutine,而且缓冲区有空闲空间,sender直接将数据放入环形缓冲区中。

在这里插入图片描述

代码如下:

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	...
    // 这个条件不成立
	if sg := c.recvq.dequeue(); sg != nil {
		...
	}
	
    // 判断缓冲区是否满了,在此处没有满
	if c.qcount < c.dataqsiz {
        // 获取chan中的缓冲区下一个可发送的空闲位置
		qp := chanbuf(c, c.sendx)
		...
        // 将数据拷贝到缓冲区中
		typedmemmove(c.elemtype, qp, ep)
        // sendx 也就是下一个可发送的位置索引+1
		c.sendx++
        // 环形队列,sendx == dataqsiz时,重置为0
		if c.sendx == c.dataqsiz {
			c.sendx = 0
		}
        // 缓冲区中元素数量+1
		c.qcount++
        
		unlock(&c.lock)
		return true
	}

	...
}

 

3.3 休眠等待

休眠等待的情形如下:此时缓冲区满了或者没有缓冲区(无缓冲chan),可能还有其它的sender正在等待发送数据。由于没有goroutine正在接收数据而且缓冲区没有空闲位置,因此当前发送的goroutine会进入chan的发送队列中休眠。休眠结束后,不用再将数据发送给接收的goroutine或者放入缓冲中,因为在被唤醒前,接收goroutine已经将数据取走了。

在这里插入图片描述

代码如下:

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	...
	// 发送队列中没有goroutine
	if sg := c.recvq.dequeue(); sg != nil {
		...
	}
	
    // 当前缓冲区已满
	if c.qcount < c.dataqsiz {
		...
	}

	...

	// 获取当前goroutine对应的g结构体
	gp := getg()
    // 获取当前g的包装sudog
	mysg := acquireSudog()
	...
	// 将发送的数据存入sudog的elem指针中,以便有接收协程可以直接取走数据
	mysg.elem = ep
	...
    // 将当前sudog放入发送队列中
	c.sendq.enqueue(mysg)
	
    ...
    // 休眠当前goroutine
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
	
    ...
    
    // 休眠结束后无需发送数据,因为数据已经被取走
    // 释放sudog
	releaseSudog(mysg)
	
    ...
    
	return true
}

 

3.4 chan发送源码

整体代码如下:

// c <- x 是语法糖,在编译时会使用该函数替代
func chansend1(c *hchan, elem unsafe.Pointer) {
	chansend(c, elem, true, getcallerpc())
}

/* c: hchan结构体指针
 * ep: 要发送的数据指针 
*/
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	// 对一个未初始化的chan发送数据,如果是阻塞模式发送,将导致阻塞
    if c == nil {
		if !block {
			return false
		}
		gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}

	...  // 省略了调试相关代码

	// 如果是非阻塞模式而且当前chan没有关闭而且缓冲区满了,直接返回
    if !block && c.closed == 0 && full(c) {
		return false
	}

	...

    // 加锁,保护hchan中的其它字段
	lock(&c.lock)
	
    // 如果chan被关闭了,向关闭的chan写数据,直接panic
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}
	
    // 1.直接发送:如果接收者队列不为空,直接将要发送的数据交给接收者,而不存入缓冲区中
	if sg := c.recvq.dequeue(); sg != nil {
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}
	
    // 2.放入缓冲区:对于缓冲型的chan,没有接收者正在接收数据,而且缓冲区还有空间,将数据放入缓冲区
	if c.qcount < c.dataqsiz {
		// qp为指向缓冲区中下一个空闲位置的指针
        qp := chanbuf(c, c.sendx)
		... 
        // 将数据拷贝到缓冲区中
		typedmemmove(c.elemtype, qp, ep)
		c.sendx++
		if c.sendx == c.dataqsiz {
			c.sendx = 0
		}
		c.qcount++
		unlock(&c.lock)
		return true
	}

    // 当前没有合适的缓冲区来存储发送的元素. 这个时候需要根据 "是否是阻塞发送" 来做出抉择
    // 如果是 "阻塞发送", 那么就需要休眠当前的 goroutine
    // 如果是 "非阻塞发送", 那么就快速失败返回(false) 
	if !block {
		unlock(&c.lock)
		return false
	}
    
	// 3.休眠等待: channel 满了,发送方会被放入发送队列,并休眠。
    
    // 获取当前goroutine
	gp := getg()
    // 获取g的包装sudog
	mysg := acquireSudog()
    // 对sudog中的字段进行赋值
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
    // 将要发送的数据直接存放再sudog的elem中,以便发送数据的协程可以直接获取数据
	mysg.elem = ep
	mysg.waitlink = nil
	mysg.g = gp
	mysg.isSelect = false
	mysg.c = c
	gp.waiting = mysg
	gp.param = nil
    
    // 将当前goroutine加入等待发送队列
	c.sendq.enqueue(mysg)
	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
    // 释放sudog
	releaseSudog(mysg)
	if closed {
		if c.closed == 0 {
			throw("chansend: spurious wakeup")
		}
		panic(plainError("send on closed channel"))
	}
	return true
}

// send处理向一个空的channel的发送操作
// 发送者要发送的ep被拷贝到接收者的goroutine
// 之后,接收者会被唤醒
// channel c肯定是空的(因为等待接收者的队列不为空)
// channel c肯定是被上了锁的,发送完成后使用lockf解锁
// sg为从接收者队列中取出的一个goroutine,它一定从c中取出来了,也就是c中没有它了
// ep为要发送的元素,ep一定不是空的,一定是指向堆或调用者的栈的
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
	... // 调试相关代码
    
    // 将数据发送给接收的协程
	if sg.elem != nil {
		sendDirect(c.elemtype, sg, ep)
		sg.elem = nil
	}
	gp := sg.g
	unlockf()
	gp.param = unsafe.Pointer(sg)
	sg.success = true
	if sg.releasetime != 0 {
		sg.releasetime = cputicks()
	}
    
    // 唤醒接收协程
	goready(gp, skip+1)
}

// 向一个无缓冲的或有缓冲但是缓冲区为空的channel的发送和接收操作都会导致一个正在运行的goroutine
// 直接向另一个正在运行的goroutine的栈上写入数据。
// 由于 GC 假设对栈的写操作只能发生在 goroutine正在运行中并且由当前goroutine来写。
// 如果违反了该规则,使用写屏障将会是有效的,但是写凭证必须能起作用才行。
// 在typedmemmove中会调用typeBitsBulkBarrier,但是如果目标数据不在堆中,将会不起作用。
// 因此我们会使用memmove和typeBitsBulkBarrier来代替
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
	
    // 直接进行内存"搬迁"
	// 如果目标地址的栈发生了栈收缩,当我们读出了 sg.elem 后
	// 就不能修改真正的 dst 位置的值了
	// 因此需要在读和写之前加上一个屏障
    dst := sg.elem
	typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
	
    // 拷贝数据
	memmove(dst, src, t.size)
}

 

chan的发送策略:

  • 如果recvq有等待接收的goroutine,直接将数据拷贝给它。
  • 如果队列中没有等待接收的goroutine且当chan是缓存型的而且缓冲区有空闲位置时,将数据拷贝到缓冲区中。
  • 如果缓冲区中没有位置,就将当前goroutine加入等待发送队列中,挂起当前goroutine,直到当前goroutine被唤醒。

 

4、接收数据 <- c

接收操作有两种, 一种带 “ok”,表示 chan 是否关闭; 一种不带 “ok”, 这种写法, 当收到相应类型为零值时无法知道是真实发送者发送的值, 还是 chan 被关闭后, 返回接收者的默认类型的零值. 两种写法, 都有各种使用的场景.

  • <- c 是一个语法糖
  • 编译阶段,i <- c 被转化为runtime.chanrecv1()
  • 编译阶段,i, ok <- c 被转化为runtime.chanrecv2()
  • 两者最后都会调用chanrecv()函数
func chanrecv1(c *hchan, elem unsafe.Pointer) {
	chanrecv(c, elem, true)
}

//go:nosplit
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
	_, received = chanrecv(c, elem, true)
	return
}

使用汇编来查看一下:

package main

import "fmt"

func main() {
	channel := make(chan int, 5)

	<- channel
	ele1, _:= <- channel
	_, ok1 := <- channel
	ele2, ok2 := <- channel
	
	fmt.Println(ele1, ele2, ok1, ok2)
}

在这里插入图片描述
上面两个函数最终都调用了chanrecv:

chan接收数据有四种情形:

  • 有等待的G,从G中接收:对于无缓冲的chan,发送者在发送数据时没有接收者,会被放入发送队列,此时接收者直接从发送者处接收数据。
  • 有等待的G,从缓存接收:对于有缓冲的chan,缓冲已满,而且发送队列中有G正在等待,从缓存中接收数据。
  • 接收缓存:缓冲中有数据,而且发送队列没有G,从缓存中接收
  • 阻塞接收::缓存中无数据,而且没有发送者,被放入接收队列并休眠。

 

4.1 有等待的G,从G中接收

这种情形如下,无缓冲的chan,有发送者在发送队列中休眠,接收者之间从发送队列中取出一个G,接收G中的数据并唤醒G。

在这里插入图片描述

代码如下:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	...
	
    // 从发送队列中取一个G
	if sg := c.sendq.dequeue(); sg != nil {
        // 寻找一个发送等待者,如果buf空间为0,之间从发送者接收数据。
        // 否则,从缓存中取出数据然后将发送者的数据放入环形缓冲区末尾
		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) {
    // dataqsiz == 0, 也就是对于无缓冲的chan
	if c.dataqsiz == 0 {
		...
		if ep != nil {
            // 直接从发送者拷贝数据
			recvDirect(c.elemtype, sg, ep)
		}
	} else {
		...
	}
    // 唤醒发送者
	goready(gp, skip+1)
}

func recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) {
	...
    
    // memmove 直接拷贝数据
	memmove(dst, src, t.size)
}

 

4.2 有等待的G,从缓存中接收

这种情形如下:对于有缓冲的chan,此时缓冲已经满了,而且有发送者在发送队列中等待,接收者从队列中取出一个发送者G,然后从缓冲区中取走数据,并将发送者的数据写入环形缓冲区的尾部并唤醒发送者。之所以这样做,是因为chan是一个队列,缓冲区中的数据肯定是来的更早的,因此要优先从缓冲区中取数据。

在这里插入图片描述

代码如下:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	...
	// 从发送队列中取出一个发送者,如果不为nil,如果有缓冲区,则要从缓冲区中取数据
	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) {
	if c.dataqsiz == 0 {
		...
	} else {     // 此时的情况是有缓冲
		
        // 环形队列已满,从队列的头部取数据。将sender的数据放入队列的尾部
		qp := chanbuf(c, c.recvx)
		...
		
        // 将数据从缓冲区拷贝到receiver中
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp)
		}
		
        // 将sender的数据拷贝到环形队列尾部
		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
	}
	sg.elem = nil
	gp := sg.g
	unlockf()
	gp.param = unsafe.Pointer(sg)
	sg.success = true
	if sg.releasetime != 0 {
		sg.releasetime = cputicks()
	}
    
    // 唤醒sender
	goready(gp, skip+1)
}

 

4.3 接收缓存

这种情形如下:对于有缓冲的chan,缓冲区中有数据,而且发送队列中没有发送者正在等待。因此,直接从缓冲区中将数据取走就可以了。

在这里插入图片描述

代码如下:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	...
	
    // 发送队列中没有发送者,此时sg == nil
	if sg := c.sendq.dequeue(); sg != nil {
		...
	}
	
    // 缓存中有数据
	if c.qcount > 0 {
		// Receive directly from queue
        // 直接从缓存中接收数据
		qp := chanbuf(c, c.recvx)
		...
        // 将数据从缓存拷贝到接收者
		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
	}

	...
}

 

4.3 阻塞接收

这种情形如下:缓冲区中没有数据或者没有缓冲区,而且发送队列中没有发送者,将接收者放入接收队列中休眠。当接收者被唤醒后,也无需再自己去接收数据,因为在被唤醒前,数据已经被唤醒我们的协程拷贝过去了。

在这里插入图片描述

代码如下:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	...
    
    // 发送队列中没有发送者
	if sg := c.sendq.dequeue(); sg != nil {
		...
	}
	
    // 而且缓冲区中没有数据
	if c.qcount > 0 {
		...
	}

	...

	
    // 没有发送者可用,阻塞在chan中
    // 获取当前g
	gp := getg()
    // 获取g的包装sudog
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
	
	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)
	
	atomic.Store8(&gp.parkingOnChan, 1)
    // 休眠当前goroutine
	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
}

 

4.4 chan接收源码

整体代码如下:

// chanrecv从channel c中接收并将数据写入到ep
// ep可能为nil,对应的就是 <- c 或 _, ok := <- c 的场景,这种情况下接收的数据会被忽略
// 如果阻塞模式为非阻塞,那么如果没有数据可接收的情况下,返回 (true,false)
// 如果,channel c被关闭了,*ep被置0,并且返回 (ture,false)
// 否则,将ep指向的内存赋值为接收到的值,并返回(true,false)
// 一个非nil的ep必须指向堆内存或调用者的栈内存
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	... // 调试相关代码

    // 如果c为ni且阻塞模式为非阻塞,直接返回(false,false)
	if c == nil {
		if !block {
			return
		}
        // 否则,接收一个 nil 的 channel,goroutine 挂起
		gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}

    
    // 在非阻塞模式下,快速检测到失败,不用获取锁,快速返回
	// 当我们观察到 channel 没准备好接收:
	// 1. 非缓冲型,等待发送列队 sendq 里没有 goroutine 在等待
	// 2. 缓冲型,但 buf 里没有元素
	// 之后,又观察到 closed == 0,即 channel 未关闭。
	// 因为 channel 不可能被重复打开,所以前一个观测的时候 channel 也是未关闭的,
	// 因此在这种情况下可以直接宣布接收失败,返回 (false, false)
	if !block && empty(c) {
		if atomic.Load(&c.closed) == 0 {
			return
		}
		if empty(c) {
			// The channel is irreversibly closed and empty.
			...
			if ep != nil {
				typedmemclr(c.elemtype, ep)
			}
			return true, false
		}
	}

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

	lock(&c.lock)

    // channel 已关闭,并且数组 buf 里没有元素
	// 这里可以处理非缓冲型关闭 和 缓冲型关闭但 buf 无元素的情况
	// 也就是说即使是关闭状态,但在缓冲型的 channel,
	// buf 里有元素的情况下还能接收到元素
	if c.closed != 0 && c.qcount == 0 {
		...
		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 {
		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true, true
	}

    // 缓冲型,buf 里有元素,可以正常接收
	if c.qcount > 0 {
        // 直接从buf中获取数据
		qp := chanbuf(c, c.recvx)
		...
        // 将数据从buf拷贝到receiver中
		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
	}

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

    // buf中无数据,而且没有发送者在等待,接收者阻塞在当前chan中
	gp := getg()
    // 获取g的包装sudog
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
	
    // 对sudog的字段赋值
	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)
	
    
	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
}


// recv处理两种接收操作从一个满的chan中
// 两种操作如下:
// 1) 接收者从buf中接收数据,发送者将数据写入buf尾部,然后发送者被唤醒继续自己的业务
// 2) 接收者直接从发送者处接收数据。
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
	if c.dataqsiz == 0 {
		...
		if ep != nil {
			
            // 直接从sender中拷贝数据
			recvDirect(c.elemtype, sg, ep)
		}
	} else {
        // buf已满,从中取出一个数据,然后将sender的数据存入,唤醒sender
        
		qp := chanbuf(c, c.recvx)
		...
		
        // 接收者从buf中拷贝数据
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp)
		}
        
        // 从发送者拷贝数据到buf中
		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
	}
	sg.elem = nil
	gp := sg.g
	unlockf()
	gp.param = unsafe.Pointer(sg)
	sg.success = true
	if sg.releasetime != 0 {
		sg.releasetime = cputicks()
	}
    
    // 唤醒sender
	goready(gp, skip+1)
}

 

5、关闭chan

func closechan(c *hchan) {
	// 关闭一个为nil的chan引发panic
    if c == nil {
		panic(plainError("close of nil channel"))
	}

	lock(&c.lock)
    // 关闭一个已经关闭的chan也会引发panic
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("close of closed channel"))
	}

	...

	c.closed = 1

	var glist gList
	
    // 释放所有的接收者
	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)
	}

    // 释放所有的发送者,它们会引发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)

	// 唤醒glist中的g
	for !glist.empty() {
		gp := glist.pop()
		gp.schedlink = 0
		goready(gp, 3)
	}
}

在关闭chan的源码中,如果向一个nil或已经关闭的chan再次调用close都会引发panic

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值