万余字带你认识 go chan 底层是如何设计的

本文深入剖析了Go语言中Channel的数据结构及工作原理,包括发送、接收和关闭等核心操作的源码解析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

golang channel 源码解析

欢迎大家关注微信公众号后端开发杂谈, 这里有更多实用的干货等着你来拿!

简介: channel 是 go 当中一个经常使用到的数据结构, 它经常用在并发通信当中. 如果只是知道它的用法, 而不了解其底层的原理是一个很大的损失. 本文带你去深入理解 chan 底层设计的原理.

数据结构

type hchan struct {
	qcount   uint           // chan 里元素的数量
	dataqsiz uint           // chan 底层循环数组的长度
	buf      unsafe.Pointer // 指向底层循环数组的指针, 只针对缓存型 chan. 内存肯定连续
	elemsize uint16 // chan 中元素大小
	closed   uint32 // chan 是否被关闭的标志
	elemtype *_type // chan 在元素类型 (element type)
	sendx    uint   // 已发送元素在循环数组中的索引 (send index)
	recvx    uint   // 已接收元素在循环数组中的索引 (receive index)
	
	// 注意: recvq, sendq 都表示被阻塞的 goroutine, 这些 goroutine 由于尝试读取 channel 或者 向
	// channel 发送数据而被阻塞 
	recvq    waitq  // 等待接收的 goroutine 队列 (list of recv waiters)
	sendq    waitq  // 等待发送的 goroutine 队列 (list of send waiters)

	// lock保护了hchan中的所有字段, 以及被此 channel blocked 的 sudog 中的几个字段.
    //
    // 不要在持有此锁的同时更改另一个G的状态(特别是, 处于ready状态的G), 因为这可能会因堆栈收缩而导致死锁.
	lock mutex
}

// waitq 是 sudog 的一个双向链表, 而 sudog 实际上是对 goroutine 的一个封装.

type waitq struct {
    first *sudog
    last  *sudog
}

// 常量

const (
    maxAlign  = 8
    // 在 64 位系统上 hchan 大小为 96, hchanSize=96
    // 在 32 位系统上 hchan 大小为 48, hchanSize=48
    hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1))
)

相关函数

// 获取buffer上第i个slot的位置. 建立在内存连续的基础上
func chanbuf(c *hchan, i uint) unsafe.Pointer {
	return add(c.buf, uintptr(i)*uintptr(c.elemsize))
}

func (c *hchan) raceaddr() unsafe.Pointer {
	// 将 cahn 上的读取和写入操作视为在此地址发生.
	// 避免使用qcount或dataqsiz的地址. 
	// 因为内置的len()和cap()会读取这些地址, 并且我们不希望它们与close()之类的操作竞争.
	return unsafe.Pointer(&c.buf)
}

创建 chan

一般而言, 使用 make 创建一个能收能发的通道:

// 无缓冲通道
ch1 := make(chan int)

// 有缓冲通道
ch2 := make(chan int, 10)

通过汇编分析, 最终创建 chan 的函数是 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")
	}
    
    // 计算 elem.size 与 size 的乘积是否溢出
	mem, overflow := math.MulUintptr(elem.size, uintptr(size))
	if overflow || mem > maxAlloc-hchanSize || size < 0 {
		panic(plainError("makechan: size out of range"))
	}

	// 当存储在buf中的元素不包含指针时, hchan将不包含GC感兴趣的指针. buf指向相同的分配, elemtype是可持久化的.
    // sudog 从它们自己的线程中引用, 因此无法将其收集.
	var c *hchan
	switch {
	case mem == 0:
		// elem.size*size 结果为0, 要么是 elem.size 为0, 要么是创建的size为0
		// 此时只分配 hchan 大小的内存.
		c = (*hchan)(mallocgc(hchanSize, nil, true))
		// Race detector uses this location for synchronization.
		c.buf = c.raceaddr()
	case elem.ptrdata == 0:
		// elem 当中不包含指针. hchan和buf的地址是连续的. 此时分配 hchanSize + mem
		c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
		c.buf = add(unsafe.Pointer(c), hchanSize) // buf 的地址的在 hchan 之后
	default:
		// elem 包含指针, 两次内存操作. hchan 和 buf的地址是不连续的
		c = new(hchan)
		c.buf = mallocgc(mem, elem, true)
	}

	c.elemsize = uint16(elem.size)
	c.elemtype = elem
	c.dataqsiz = uint(size)

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

例如, 当我们创建一个 size 为 6, 元素类型为 int 的 chan, 其内存结构如下:

在这里插入图片描述

发送

// 如果 block 不为 true, 则该 protocol 不会休眠
// 当关闭与休眠有关的chan时, 可以使用 g.param == nil 唤醒休眠. 
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    // channnl 为nil时, 当进行非阻塞状况下(即 block 为 false), 立即返回, 否则永远休眠当前的 goroutine 到永远.
	if c == nil {
		if !block {
			return false
		}
		gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}
    
    // 开启了 -race 调试 
	if raceenabled {
		racereadpc(c.raceaddr(), callerpc, funcPC(chansend))
	}

	// 快速判断: 在非阻塞模式下, 快速检测到失败, 不用获取锁, 快速返回(false)
    // 
    // 如果 chan 未关闭且 chan 没有多余的缓冲空间. 这可能是:
    // 1. chan 是非缓冲型的, 且等待接收队列里没有 goroutine
    // 2. chan 是缓冲型的, 但缓冲队列已经装满了元素
    // 
    // c.closed == 0, chan 未关闭
    // c.dataqsiz == 0 && c.recvq.first == nil, 非缓冲型 chan, 且 "接收队列(recvq)" 当中没有元素
    // c.dataqsiz > 0 && c.qcount == c.dataqsiz, 缓存型 chan, 且 "缓冲队列(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() // 记录当前的 cpu 的 ticket
	}

    // 加锁状况下操作.
	lock(&c.lock)
    
    // 当前的 chan 已经关闭, 那当然就无法发送了!!
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}
    
    // 从接收队列当中寻找一个接收者, 直接将当前的内容发送给这个接收者, 绕过缓存区
	if sg := c.recvq.dequeue(); sg != nil {
	    // 找到了等待的接收者. 可以绕过通道缓冲区(如果有)将要发送的值直接发送到接收者.
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}
    
    // 对于缓冲型 chan, 还存在缓冲空间
	if c.qcount < c.dataqsiz {
		// 存在可用的缓冲区, 将发送元素入队列
		qp := chanbuf(c, c.sendx)
		
		// 开启 -race
		if raceenabled {
			raceacquire(qp)
			racerelease(qp)
		}
		
		// 数据移动
		typedmemmove(c.elemtype, qp, ep)
		
		// 修正 sendx 和 qcount 变量
		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
	}

	// 阻塞当前的 goroutine 
	gp := getg()
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
	
	// 在"分配elem"和"将mysg入队到gp.waitcopy可以找到它的地方" 之间没有堆栈拆分.
	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) // 将当前的 sudog 入队列 sendq
	
	// 带 lock 休眠的休眠操作, 意味着在 goparkunlock 需要解锁 lock
	goparkunlock(&c.lock, waitReasonChanSend, traceEvGoBlockSend, 3)
	
	// 确保 "发送的值" 保持活跃状态, 直到接收者将其复制出来. 也就是延长发送的值的生存周期, 防止提前被 GC 回收
	// sudog具有指向堆栈对象的指针, 但是sudog不被视为stack跟踪器的root.
	KeepAlive(ep)
    
    // 唤醒后的一些检查和清理操作
	if mysg != gp.waiting {
		throw("G waiting list is corrupted")
	}
	gp.waiting = nil
	
	// 注意: 这里的 gp.param 的值是 mysg
	if gp.param == nil {
	    // 唤醒的以后, 发现 chan 已经关闭了, 那么发送肯定会存在问题
		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
}
// send 是将发送方发送的ep值复制到接收方sg中(跳过缓冲区). 然后将接收方sg唤醒, 继续执行后续的操作.
//
// chan c 必须为空且已锁定. 使用unlockf()在发送后解锁c.
// sg 必须已经从 recvq 中出队.
// ep 必须为非nil, 并且指向堆或调用者的堆栈.
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    // 开启了 -race 追踪 
	if raceenabled {
		if c.dataqsiz == 0 {
			racesync(c, sg)
		} else {
			qp := chanbuf(c, c.recvx)
			raceacquire(qp)
			racerelease(qp)
			raceacquireg(sg.g, qp)
			racereleaseg(sg.g, qp)
			c.recvx++
			if c.recvx == c.dataqsiz {
				c.recvx = 0
			}
			c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
		}
	}
	
	if sg.elem != nil {
		sendDirect(c.elemtype, sg, ep) // 直接发送
		sg.elem = nil
	}
	
	gp := sg.g
	unlockf()
	
	// 唤醒正在休眠的接收者
	gp.param = unsafe.Pointer(sg)
	if sg.releasetime != 0 {
		sg.releasetime = cputicks()
	}
	goready(gp, skip+1)
}

// 直接发送

// 仅在 "一个正在运行的goroutine写入到另一个正在运行的goroutine的stack" 的操作是在 "无缓冲" 或 "空缓冲" chan 上
// 发送和接收.
// 
// GC假定 stack 写入仅在goroutine运行时发生, 并且仅由该goroutine完成. 
// 使用写屏障足以弥补违反该假设的缺点, 但是写屏障必须起作用.
// typedmemmove 将调用 bulkBarrierPreWrite, 但是目标字节不在 heap 中, 因此这无济于事. 
// 因此这里安排调用 memmove 和 typeBitsBulkBarrier.
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
	// src在我们的stack上, dst是另一个stack上的插槽.
    
    // 一旦我们从sg中读出了sg.elem, 如果目标 stack 被复制(缩小), 它将不再被更新.
    // 因此, 请确保在 read 和 use 之间没有任何抢占点.
	dst := sg.elem
	typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
	
	// 不需要go写屏障检查, 因为dst始终是Go内存.
	memmove(dst, src, t.size)
}

typedmemmove() VS memmove()

typedmemmove() 是将类型 _type 的对象从 src 拷贝到 dst, src和dst都是unsafe.Pointer, 这当中涉及到了内存操作.
由于GC的存在, 在拷贝前, 如果 _type 包含指针, 需要开启写屏障 bulkBarrierPreWrite. 然后调用 memmove() 进行内存
拷贝的操作.

memmove() 直接是进行内存移动, 使用的是汇编实现的, 代码位置在 src/runtime/memmove_amd64.s. 当然了, 这个操作
本身就是单纯的内存操作, 不涉及其他任何内容.


bulkBarrierPreWrite(dst, src, size uintptr)

bulkBarrierPreWrite 使用[dst, dst + size) 的 pointer/scalar 信息对内存范围[src, src + size) 中的每个指针
slot 执行写屏障. 这会在执行 memmove 之前执行必要的写屏障.

src, dst和size必须是 pointer-aligned.
范围 [dst,dst + size) 必须位于单个对象内.
它不执行实际的写操作.

作为一种特殊情况, src == 0 表示正在将其用于memclr. bulkBarrierPreWrite将为每个写障碍的src传递0.

调用方应在调用memmove(dst,src,size) 之前立即调用bulkBarrierPreWrite. 此函数标记为 nosplit, 以避免被抢占; GC
不得停止memmove和执行屏障之间的goroutine. 调用者还负责go指针检查, 这可能是将Go指针写入非Go内存中.

对于分配的内存中完全不包含指针的情况(即src和dst不包含指针), 不维护 pointer bitmap; 通常, 通过检查 typ.ptrdata/
bulkBarrierPreWrite 的任何调用者都必须首先确保基础分配包含指针.


typeBitsBulkBarrier(typ *_type, dst, src, size uintptr)

typeBitsBulkBarrier 对将由 memmove 使用 bitmap 类型定位这些指针slot的每个指针从[src, src+size) 复制到
[dst, dst+size) 的每个指针执行写屏障.

类型typ必须与[src, src+size) 和 [dst, dst + size) 完全对应.
dst, src和size必须是 pointer-aligned.
类型typ必须具有plain bitmap, 而不是GC程序.
此功能的唯一用途是在 chan 发送中, 并且64kB chan元素大小限制为我们解决了这一问题.

不能被抢占, 因为它通常在 memmove 之前运行, 并且GC必须将其视为原子动作.

接收

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

编译器对于上述两种写法的处理最终会转换成源码里的两个函数:

// 不带"ok"类型, 例如 val := <-ch
func chanrecv1(c *hchan, elem unsafe.Pointer) {
    chanrecv(c, elem, true)
}

// 带"ok"类型, 例如 val, ok := <-ch
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
    _, received = chanrecv(c, elem, true)
    return
}

最终的处理函数都是 chanrecv.

// chanrecv在chan c上接收并将接收到的数据写入 ep.
// ep可能为nil, 在这种情况下, 接收到的数据将被忽略.
// 
// 如果 block == false 并且 没有可用元素, 则返回(false,false).
// 否则, 如果 c 关闭, 则*ep为零并返回(true, false).
// 否则, 用一个元素填充 *ep并返回(true, true)
// ep 必须是非nil且指向heap或调用者的stack.
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // raceenabled: 不需要检查ep, 因为它始终在stack中, 或者是反射所分配的新内存.
    
    // 当前的 chan 为nil, 在非阻塞的状况下, 立即返回 (false,false), 否则阻塞当前的 goroutine 到永远
	if c == nil {
		if !block {
			return
		}
		
		// 当前的 goroutine 永远休眠下去
		gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}

	
	// 快速判断: 在非阻塞模式下, 快速检测到失败, 不用获取锁, 快速返回(false, false)
    //
    // 当我们观察到 chan 没准备好接收:
    // 1. 非缓冲型, 等待发送队列 sendq 里没有 goroutine 在等待
    // 2. 缓冲型, 在缓冲队列 buf 当中没有元素
    // 
    // 之后, 又观察到 closed == 0, 即 chan 关闭了.
    // 由于 chan 不可能被重复打开, 所以前一个观察的时候 chan 也是未关闭的,  因此在这种状况下直接接收失败, 
    // 返回 (fasle, false)
    //
    // 这里操作顺序在这里很重要: 在进行近距离追踪时, 反转操作可能导致错误的行为.
    //
    // c.dataqsiz == 0 && c.sendq.first == nil, 非缓冲型chan, 当前的 "发送队列(sendq)" 为空
    // c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0 缓冲型chan, 当前的 "缓冲队列(buf)" 为空
    // atomic.Load(&c.closed) == 0, chan 已关闭
	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)
    
    // 当前 chan 关闭的状况下, 并且"缓存队列(buf)"里面没有元素. 
    // 也就是说, 即使是关闭状态, 对于缓冲型的 chan, "缓冲队列(buf)" 里面的元素依旧可以被接收到(qcount>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
	}
    
    // 阻塞的 "发送队列(sendq)" 当中存在 sender. 则说明当前的缓冲队列已满.
    // 可能的状况:
    // 1. 非缓冲型的 chan
    // 2. 缓冲型的 chan, 但 buf 已经满了
    // 针对1, 直接内存拷贝
    // 针对2, 接收到 buf 的头元素(recvx), 并将发送者的元素放到 buf 的尾部(sendx) (这里sendx和recvx其实值是
    // 一样的), 然后唤醒休眠的发送者.
	if sg := c.sendq.dequeue(); sg != nil {
	    // 详细参考 recv 当中的解释
		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true, true
	}
    
    // 缓冲型, 当前的缓存队列存在元素
	if c.qcount > 0 {
		qp := chanbuf(c, c.recvx)
		if raceenabled {
			raceacquire(qp)
			racerelease(qp)
		}
		
		//  将 qp 的数据拷贝到 ep
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp)
		}
		typedmemclr(c.elemtype, qp) // 清除 qp 内容
		
		// 修正 recvx 和 qcount 的值
		c.recvx++
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		c.qcount--
		unlock(&c.lock)
		return true, true
	}
    
    // 当前缓队列没有任何元素, 隐含着 "发送队列(sendq)为空".
    // 非阻塞状况下(block为false), 快速返回 (false, false)
    // 阻塞状况下(block为true), 休眠当前的 goroutine
	if !block {
		unlock(&c.lock)
		return false, false
	}
    
    // 休眠当前的 goroutine
	gp := getg()
	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) // 将当前的 sudog 入接收队列 recvq
	
	// 持有锁的状况下休眠
	goparkunlock(&c.lock, waitReasonChanReceive, traceEvGoBlockRecv, 3)
    
    // 休眠被唤醒的后需要的验证和清理工作
	if mysg != gp.waiting {
		throw("G waiting list is corrupted")
	}
	gp.waiting = nil
	if mysg.releasetime > 0 {
		blockevent(mysg.releasetime-t0, 2)
	}
	closed := gp.param == nil // gp.param 的值应该是 mysg. 只有在关闭的时候这个值才被设为 nil
	gp.param = nil
	mysg.c = nil
	releaseSudog(mysg)
	return true, !closed
}
// recv 在 full chan(缓冲通道已满或者非缓冲通道) c上处理接收操作.
// 包含2个部分:
// 1) 由发送方 sg 发送的值被放入通道, 并且唤醒发送方以继续执行其后续的逻辑.
// 2) 接收方接收到的值(当前G)被写入 ep.
// 对于同步通道, 两个值相同.
// 对于异步通道, 接收方从通道缓冲区获取其数据, 而发送方的数据放入通道缓冲区.
//
// chan c 必须已满并且已锁定. recv用 unlockf 函数解锁c.
// sg 必须已经从 sendq 中出队列.
// ep 必须为非nil, 并且指向 heap 或调用者的stack.
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
	if c.dataqsiz == 0 {
	    // 非缓冲型通道, 或者同步通道.
	    // 设置 -race
		if raceenabled {
			racesync(c, sg)
		}
		if ep != nil {
			recvDirect(c.elemtype, sg, ep) // 直接从 sender 复制数据.
		}
	} else {
	    // 缓冲型通道, 或者异步通道
	    // 
        // 先获取 recvx 对应的 slot, 
        // 先 slot 当中读取数据到 ep 当中, 然后将 sender 发送的数据再插入到 slot 当中.
        // 由于缓冲队列是满的, 所以经过上述的操作之后, 缓冲队列依旧是满的, 但是 recvx 需要向前移动一位(保证chan的元素
        // 都是先进先出的), 而且 sendx 也是需要向前移动一位. 但是 sendx 和 recvx 的值相等.
		
		// 获取 slot 的位置
		qp := chanbuf(c, c.recvx)
		
		// copy data from "buf" to "receiver"
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp)
		}
		// copy data from "sender" to "buf"
		typedmemmove(c.elemtype, qp, sg.elem) 
		
		// 更新 recvx 和 sendx
		c.recvx++
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
	}
	
	// sg 清理工作
	sg.elem = nil
	gp := sg.g
	unlockf()
	
	// 唤醒对应的发送者
	gp.param = unsafe.Pointer(sg)
	if sg.releasetime != 0 {
		sg.releasetime = cputicks()
	}
	goready(gp, skip+1)
}
// 原理和 sendDirect 类似
func recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) {
	src := sg.elem
	typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
	memmove(dst, src, t.size)
}

recvq 的数据结构如下:

在这里插入图片描述

从整体上来看一下此时 chan 的状态:

在这里插入图片描述

G1 和 G2 被挂起了, 状态是 WAITING.

关闭

func closechan(c *hchan) {
    // 当前的 chan 为 nil. 关闭自然会报错
	if c == nil {
		panic(plainError("close of nil channel"))
	}

	lock(&c.lock)
	
	// 重复关闭一个 chan
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("close of closed channel"))
	}
    
    // 设置了 -race 追踪
	if raceenabled {
		callerpc := getcallerpc()
		racewritepc(c.raceaddr(), callerpc, funcPC(closechan))
		racerelease(c.raceaddr())
	}
    
    // 关闭
	c.closed = 1

	var glist gList // 将所有处于阻塞状态的 g 串在一起. 链接点是 g.schedlink

	// 释放所有的 recvq
	for {
		sg := c.recvq.dequeue()
		if sg == nil {
			break
		}
		if sg.elem != nil {
			typedmemclr(c.elemtype, sg.elem) // 清除 sg 当中的元素值
			sg.elem = nil
		}
		if sg.releasetime != 0 {
			sg.releasetime = cputicks()
		}
		gp := sg.g
		gp.param = nil // 关闭的标志之一
		if raceenabled {
			raceacquireg(gp, c.raceaddr())
		}
		glist.push(gp) // 将 goroutine 存入 glist
	}

	// 释放所有的 sendq (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 = nil
		if raceenabled {
			raceacquireg(gp, c.raceaddr())
		}
		glist.push(gp) // 将 goroutine 存入 glist
	}
	unlock(&c.lock)

	// 唤醒 glist 当中所有的 gp. 这样做的好处是减少锁定的时间
	for !glist.empty() {
		gp := glist.pop()
		gp.schedlink = 0
		goready(gp, 3)
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值