go channel 探索

48 阅读15分钟

前言

此篇笔记主要以自己的浅薄见解去讨论 go channel 数据结构的底层设计, 目前包括: 数据结构, makechan操作, send操作, recv操作, close操作, 并伴随着代码走读形式进行记录. 在过程中会跳过一些跟文章主题相关的关系不大代码,着重核心代码的解释.

golang倡导 通过通信共享内存, 而不是通过共享内存进行通信.
通过共享内存进行通信 在形式上表现为两个协程通过共享一个外部变量(如全局变量)来实现信息的共享 通过通信共享内存 便是此篇笔记的主角, channel

一些约定

后面的源码展示中出现// ... [explaination]格式的注释, 表示这里有被忽略的代码, 并可能会附上这段代码的一些解释

一些出现的名词解释:
sender: 参与channel读操作的goroutine
receiver: 参与channel写操作的goroutine
g: 指goroutine
buffer: channel缓冲队列

数据结构

channel分为非缓冲channel缓冲channel
创建如下:

  // 必须要通过make创建, 使用一个nil channel 会panic
  noCacheChannel := make(chan int) // 非缓冲channel
  cacheChannel := make(chan int, 3) // 缓冲channel

两者区别为非缓冲channel遵守一进一出原则, 如果没有立即读数据, sender则会block, 反之, 没有立即写数据, receiver会block. 而缓冲channel则有个buffer 区域临时堆放数据, 就算receiver没有及时接收数据, 数据会放在buffer里, sender不会block, 直到buffer区域堆满为止, 而receiver则会不断从buffer读取数据直到buffer为空.

数据结构:

type hchan struct {
	qcount   uint // 当前buffer数据大小      
	dataqsiz uint // buffer数据容量          
	buf      unsafe.Pointer // buffer内存指针
	elemsize uint16 // 数据类型占用内存大小
	closed   uint32 // channel关闭状态
	elemtype *_type // 数据类型
	sendx    uint  // buffer 头指针(sender待发送的数据从头部插入)
	recvx    uint  // buffer 尾指针(receiver待接收的数据从尾部插入)
        
  // 等待队列, 存储对应休眠的routine上下文, waitq 本质是一个双向链表
	recvq    waitq  // receiver等待队列, 先进先出原则
	sendq    waitq  // sender等待队列, 先进先出原则

	lock mutex // 锁
}

其中buffer本身是一个环形链表, 数据从头部进列, 尾部出列, 即发送和接收也是按照FIFO进行的, 当sendx == recvx 时, 意味着buffer当前处于是full或者empty

如下图为一个 存储了3个数据的 4-buffer channel:

channel_v2.png recvq和sendq分别为receiver和sender的休眠队列, 所有因channel而bolck的g都会放在这两个队列里等待唤醒

makechan解读

即通过make标识符进行通道创建触发的底层代码:

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

	// ... 编译器检查代码
      
	var c *hchan
	switch {
	case mem == 0: // mem 代表需要额外分配的buffer内存, 进入case说明是 no-buffer-channel
		c = (*hchan)(mallocgc(hchanSize, nil, true)) // 仅分配 hchan自身所需要的内存
		c.buf = c.raceaddr() // 没有缓冲区但为了应付竞态检测, 该值没有意义
	case elem.PtrBytes == 0: // 需要额外分配的buffer没有包含指针变量时进入该case
		c = (*hchan)(mallocgc(hchanSize+mem, nil, true)) // hchan和buffer内存一起分配
		c.buf = add(unsafe.Pointer(c), hchanSize) // 标记对应内存空间首指针
	default: // 进入此case说明buffer区域含有指针变量
		c = new(hchan)
		c.buf = mallocgc(mem, elem, true) // 此时buffer需要独立分配, 因为mallogc方法需要带类型, gc才会进行扫描
	}
        
    // 参数初始化
	c.elemsize = uint16(elem.Size_)
	c.elemtype = elem
	c.dataqsiz = uint(size)
	lockInit(&c.lock, lockRankHchan)
	}
	return c
}

首先我们先看下定义,该方法接收一个类型(*type)和容量(size), 返回对应的实例(hchan)

而代码核心在中间switch条件判断那里:

  • 第一个case说明当前是一个无缓冲channel, 故只需分配channel自身的内存即可
    不过值得关注的是mallocgc的定义

    func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer

    第二个参数是类型, 如果这个参数为nil, 说明channel自身是不需要gc扫描的, 也就是说里面是没有引用变量的, 下面引用一段官方注释, 解释了这种情况:

    // 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.
    

    也就是说只要buf中的elem不包含指针, 实际上hchan是不需要扫描的, 直接回收即可(没有引用到其他内存), 而唯一的指针*_type是随着程序的启动永久放在内存中的, 不需要gc回收.

  • 第二个case是一个优化, 当buffer里面没有包含指针变量时, 只需分配一次内存即可, buffer会跟着hchan一起被回收
    这里的没有包含指针变量是指elem本身不是指针或elem里没有包含指针, 如果一个结构体里有指针成员则会被识别带有指针, 这里感兴趣的可以通过debug观察hchan.elemtype.PtrBytes是不是为0来判断

image.png

  • 第三个case, 当buffer包含指针变量时, 需要被gc扫描, 所以buffer需要单独分配内存, 此时需要进行两次内存分配

send 解读

有两种调用方式:

   // ---one 直接使用
   ch := make(chan int)
   ch <- 1 // 这里receiver没有及时接收会block
   
   // ---two 配合select使用
   select {
    case ch <- 1:
    default:
       fmt.Println("no receiver, but no block") // 这里case分支走不通时总能跑到default, 故不会block
   }

核心源码:

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {

   // -------section 1. nil channel
	if c == nil {
            if !block { // 如果channel未初始化, no-block mode下, 则不会panic
                    return false
            }
               
            // 丢出panic
            gopark(nil, nil, waitReasonChanSendNilChan, traceBlockForever, 2)
            throw("unreachable")
	}

   // ...
   
   // -------section 2 fast path
	if !block && c.closed == 0 && full(c) {
		return false
	} // no-block mode下, 通道已满, 返回nil

	// ...
        
	lock(&c.lock)

   // -------section 3 check closed
	// 如果通道已经close了, panic
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}
   
   // -------section 4 exist reciver            
   // 存在等待中的receiver, 将数据传递过去, 唤醒该g
	if sg := c.recvq.dequeue(); sg != nil {
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}
  
  // -------section 5 buffer is no full
  // buffer有空闲的空间, 将数据放进buffer
	if c.qcount < c.dataqsiz { // 有缓存情况
		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
	}
    
   // -------section 6 g need to block
   // 跑到这里表示数据没有归处, 这里需要block了
                         
	if !block { // no-block mode 下, 直接结束
		unlock(&c.lock)
		return false
	}

	gp := getg()
	mysg := acquireSudog()
                                
	// ... 将当前g和上下文封装进mysg(sudog), 包括待发送的值指针, 放在mysg.elem
                                
	c.sendq.enqueue(mysg) // 进入sender等待队列
   // 当前g 休眠
	gp.parkingOnChan.Store(true)
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceBlockChanSend, 2)
    // -------section 7 g wake up
    // 当前g 唤醒
                                
	// ... 回收sudog
   
   //醒来发现通道已close的处理
	if closed {
		if c.closed == 0 {
			throw("chansend: spurious wakeup")
		}
		panic(plainError("send on closed channel"))
	}
	return true
}

老规矩, 先看定义 func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) (selected bool), 这里参数依次为 channel自身(c), 需传递的值(cp), 是否为no-block mode(block), 调用者的pc指针(callerpc), callerpc跟主流程关系不大, 不做讨论.
返回值适用于select情景, 表示该case分支是否block
定义这边需要着重探讨的是参数block, 如果为false, 表示在一些情况下, 将不会panic/block, 而是直接结束流程, 我这里把这种场景称为no-block mode
那什么时候需要no-block mode, 什么时候不需要呢? 这个问题就需要看其入口了:

  1. 类似 c<-x用法时, block->true
// entry point for c <- x from compiled code.
//
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
	chansend(c, elem, true, getcallerpc())
}
  1. 通过配合select使用时, block -> false, 此时是否block将由select进行控制
// compiler implements
//
//	select {
//	case c <- v:
//		... foo
//	default:
//		... bar
//	}
//
// as
//
//	if selectnbsend(c, v) {
//		... foo
//	} else {
//		... bar
//	}
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
	return chansend(c, elem, false, getcallerpc())
}

接下来是核心源码的讨论, 这里我们按照从上到下划分的区域进行讨论:

  1. section 1, channel未初始化, no-block mode: 直接返回, 否则, panic. 这里引出了channel的一个特性, 往nil channel 写入会panic, 而在select-case中则不会, 所处的case将不会执行
  2. section 2, 这里是一个no-block mode 场景下的快速判断, 在no-block mode下, 如果通道没有receiver且buffer已满, 则直接结束. 因为后面的逻辑要加锁, 所以这里其实是一个优化
func full(c *hchan) bool {
	if c.dataqsiz == 0 {
		return c.recvq.first == nil // 没有等待中的receiver
	}
	return c.qcount == c.dataqsiz // full buffer
}
  1. section 3, 往一个已关闭的channel写数据, panic
  2. section 4, 如果有等待中的receiver, 唤醒, 并将值传递给receiver, success, 这里sg(sudog)是g的一个封装, 带着上下文信息
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
   // ...
    
	if sg.elem != nil { // 有可能receiver没有接收方, 如: <-ch
		sendDirect(c.elemtype, sg, ep) //直接将值拷贝给接收方
		sg.elem = nil
	}
	gp := sg.g
	unlockf()
	gp.param = unsafe.Pointer(sg)
// sg 上下文更新
	sg.success = true
	if sg.releasetime != 0 {
		sg.releasetime = cputicks()
	}
	goready(gp, skip+1) // 唤醒 g
}
                                                    
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
	dst := sg.elem
  // 这里是直接进行两个g之间的栈拷贝, 需要对对应的接收方(dst)进行写屏障处理, 不然gc不会扫描dst 
	typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size_)
	memmove(dst, src, t.Size_) // 内存拷贝
}                                                    
  1. section 5, 有空闲的buffer, 将值暂时放到buffer, success
func chanbuf(c *hchan, i uint) unsafe.Pointer {
	return add(c.buf, uintptr(i)*uintptr(c.elemsize)) // 以随机地址的方式获取第i元素的地址
}
  1. section 6, 数据无处可去 no-block mode下, 这里直接结束, 否则, 将当前g封装, 放入sender等待队列并休眠, 直到被receiver唤醒
  2. section 7, 被唤醒, 回收sudog, 如果此时发现channel已经close, 表示是因为close操作导致的唤醒, 直接panic, 否则, 便是被receiver唤醒, success

recv解读

有以下几种调用方式:

   ch := make(chan int)
   // 如果没有sender, 会block
   v := <-ch // 有接收值
   <-ch // 无接收值
   v, ok := <-ch // 不会block, 如果通道close, 返回 nil, false, 不会panic
   
   // 配合select使用
   select {
    case <-ch:
    case v = <-ch:
    case v, ok = <-ch:
    default:
       fmt.Println("no sender, but no block") // 这里case分支走不通时总能跑到default, 故不会block

核心源码:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
   // ...
   
   // section 1: nil channel                    
	if c == nil {
		if !block {
			return
		}
		gopark(nil, nil, waitReasonChanReceiveNilChan, traceBlockForever, 2)
		throw("unreachable")
	}

	// section 2: fast path
	if !block && empty(c) {
		if atomic.Load(&c.closed) == 0 {
			return
		}
		if empty(c) {
			// ...

			if ep != nil {
				typedmemclr(c.elemtype, ep) // 将ep置为零值
			}
			return true, false
		}
	}

	// ...

	lock(&c.lock)

	if c.closed != 0 {
           // section 3: channel closed and no unhandled data
		if c.qcount == 0 {
			// ...

			unlock(&c.lock)
			if ep != nil {
				typedmemclr(c.elemtype, ep) // 将ep置为零值
			}
			return true, false
		}
	} else {
		// section 4: exist waiting sender
		if sg := c.sendq.dequeue(); sg != nil {
			recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
			return true, true
		}
	}

	// section 5: exist buffer data
	if c.qcount > 0 {
		qp := chanbuf(c, c.recvx)

		// ...

		if ep != nil { // // 有可能receiver没有接收方, 如: <-ch
			typedmemmove(c.elemtype, ep, qp) // 值拷贝
		}
		typedmemclr(c.elemtype, qp) // 清空值, 方便gc回收
		c.recvx++
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		c.qcount--
		unlock(&c.lock)
		return true, true
	}

   // section 6: asleep
	if !block {
		unlock(&c.lock)
		return false, false
	}

	// 没有数据且通道活跃, 此时休眠
	gp := getg()
	mysg := acquireSudog()

	// ... 将当前g和上下文封装进mysg(sudog), 包括待接收的值指针, 放在mysg.elem

	c.recvq.enqueue(mysg) // receiver等待队列入队
	gp.parkingOnChan.Store(true)
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceBlockChanRecv, 2) // 休眠
   // section 7: wakeup

	// ... check & 回收sudog

	return true, success
}

定义: func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool), 基本跟chansend大差不差, 不过多赘述, 明显的区别是返回多了个received, 这里指明了receiver的另一种语法:

v, ok := <-ch // ok 即是返回值received

接下来跟着核心源码讨论流程:

  1. section 1: nil channel 场景, 往nil channel读数据会panic, no-block mode下, selected 返回false

  2. section 2: no-block mode 的fastpath 场景, 这里可能有疑惑的点是三次判断: isEmpty -> isClose -> isEmpty
    如果反过来这样判断: isClose -> isEmpty, 在判断close时, channel是open, 而在判断empty时, channel是close,此时程序认为channel状态为open, empty, 而实际上应该是close, empty
    而按照源码的顺序的话, 在第二次判断时, 如果channel是open, 那就代表在第一次判断中channel应该是empty, open 状态, 而第三次判断时为了处理channel关闭时, 是否还有数据的情况. 而如果系统认为是非empty, 而实际上是empty的情况, 在后面的代码也有做二次处理.

  3. section 3: 再次检查channel状态是否close, empty, 是直接结束

  4. section 4: 是否有休眠中的sender, 有的话唤醒, 如果此时存在buffer, buffer一定是full, 此时优先从buffer取值, 并将唤醒的sender的值入列buffer, 如果是unbuffered channel, 则直接从sender取值

func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
	if c.dataqsiz == 0 { // buffer没有数据再从等待中的sender获取
		// ...

		if ep != nil { // 有可能receiver没有接收方, 如: <-ch
			recvDirect(c.elemtype, sg, ep) // 直接从sender获取
		}
	} else { // buffer 存在数据优先从buffer获取
		qp := chanbuf(c, c.recvx) // 从buffer中取出一个值

		// ...

		if ep != nil { // 有可能receiver没有接收方, 如: <-ch
			typedmemmove(c.elemtype, ep, qp) // 值拷贝
		}

           // 将sender的值拷贝到buffer
		typedmemmove(c.elemtype, qp, sg.elem)
		c.recvx++
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		c.sendx = c.recvx // 当queue为full时, sendx == recvx
	}
  //更新sg上下文并唤醒
	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 recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) {
	src := sg.elem

   // 写屏障处理                             
	typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size_)
	memmove(dst, src, t.Size_) // 值拷贝
}
  1. section 5: buffer有值, 从buffer获取

  2. section 6, 没有数据 no-block mode下, 这里直接结束, 否则, 将当前g封装, 放入receiver等待队列并休眠, 直到被sender唤醒

  3. section 7, 被唤醒, 回收sudog, success

close 解读

通过close关键字关闭channel触发:

close(ch)

channel close后, 所有等待中的senderPanic, 所有等待中的receiver收到零值
核心源码:

func closechan(c *hchan) {
	if c == nil { // close未初始化的channel, panic
		panic(plainError("close of nil channel"))
	}

	lock(&c.lock)
	if c.closed != 0 { // close已经关闭的channel, panic
		unlock(&c.lock)
		panic(plainError("close of closed channel"))
	}

	// ...

	c.closed = 1

	var glist gList
   // 预处理所有的receiver
	// release all readers
	for {
		sg := c.recvq.dequeue()
		if sg == nil {
			break
		}
		if sg.elem != nil {
			typedmemclr(c.elemtype, sg.elem) // 所有接收方值置0
			sg.elem = nil
		}

		// ...

		gp := sg.g
		gp.param = unsafe.Pointer(sg)
		sg.success = false

		// ...

		glist.push(gp)
	}
   // 预处理所有的sender
	// release all writers (they will panic)
	for {
		sg := c.sendq.dequeue()
		if sg == nil {
			break
		}
		sg.elem = nil

		//...

		gp := sg.g
		gp.param = unsafe.Pointer(sg)
		sg.success = false

		// ...

		glist.push(gp)
	}
	unlock(&c.lock)

   // 唤醒所有的sender和receiver
	// Ready all Gs now that we've dropped the channel lock.
	for !glist.empty() {
		gp := glist.pop()
		gp.schedlink = 0
		goready(gp, 3)
	}
}

这里源码相对比较简单,
首先检查, 如果关闭一个未初始化或者已关闭的channel, 将会panic; 预处理所有等待中的sender和receiver, 然后进行唤醒
如果sender因此被唤醒, 将会panic, 这部分逻辑写在chanshend里

后记

该笔记有些代码涉及到竞态检测(race), 垃圾回收, 协程模型, 内存模型等相关的内容, 这部分内容未来可能会在本账号的其他文章有所体现, 也可能不会.

初稿编辑于 2026.3.2

2026.3.3

sudog

这里补充在上续源码中出现, 也是经常在源码中出现的数据结构sudog.
先看下官方的解释:

// sudog (pseudo-g) represents a g in a wait list, such as for sending/receiving
// on a channel.
//
// sudog is necessary because the g ↔ synchronization object relation
// is many-to-many. A g can be on many wait lists, so there may be
// many sudogs for one g; and many gs may be waiting on the same
// synchronization object, so there may be many sudogs for one object.
//
// sudogs are allocated from a special pool. Use acquireSudog and
// releaseSudog to allocate and free them.

sudog 即pseudo-g, 我这里把他称为伪g, 他主要常见于一些需要将g放在一个等待队列(主要是sudog链表)中休眠, 并有序唤醒的场景, 如mutex, channel等进行block和wakeup操作时, 就会用到这个数据结构.本质上就是对g的封装并附带一些相关的上下文信息.
mutex等数据结构的等待队列主要由sema来实现, 后续如果有笔记涉及到, 会介绍.
数据结构:

type sudog struct {
// 以下参数受hchan的lock保护, 用在sema逻辑的, 受sema的lock保护, 如果没有的,表明该参数不会参与并发逻辑
	g *g // 当前g
    
   // chan 等待队列的连接指针(双向链表指针), hchan.sendq和recvq 主要存储链表的头尾指针
	next *sudog
	prev *sudog

	elem unsafe.Pointer // 简单来说elem就是在代码 `v<-ch` 或 `ch<-v` 中v的指针
   
   // 这两个时间字段猜想是用于性能检测
	acquiretime int64
	releasetime int64
    
   // 表示当前g正处于select中 
	isSelect bool
   // 表示当前channel有没有成功读/写到值, 被唤醒时success是true, 表示读/写值成功, false, 表示channel已关闭
	success bool
   waitlink *sudog // channel的读/写等待队列
   c        *hchan // channel

  // 下面字段被用在sema相关的场景
   ticket  uint32
	waiters uint16
	parent   *sudog 
	waittail *sudog 
}

acquiresudog

获取一个sudog, 由于sudog会被频繁的使用, 所以p(gmp的p)和sched(gmp中的全局队列)里会有专门的cache去缓存, 可以理解为p和sched有专门的对象池存储sudog, 下面为具体源码, 流程比较简单.

func acquireSudog() *sudog {
	mp := acquirem() // 获取当前m(gmp的m)
	pp := mp.p.ptr() // 从m获取当前p
	if len(pp.sudogcache) == 0 { // p的sudog对象池是空的
		lock(&sched.sudoglock)
		for len(pp.sudogcache) < cap(pp.sudogcache)/2 && sched.sudogcache != nil { // 如果sched的sudog对象池有
			s := sched.sudogcache
			sched.sudogcache = s.next
			s.next = nil
			pp.sudogcache = append(pp.sudogcache, s) // 从sched的sudog池中取出来填充进p的sudog池, 直到sched没有了或者p的池子容量过半了
		}
		unlock(&sched.sudoglock)
		if len(pp.sudogcache) == 0 { // 如果p和sched的池中都没有对象, new一个对象放进p的池子里
			pp.sudogcache = append(pp.sudogcache, new(sudog))
		}
	}

   // 从p中取出一个sudog 
	n := len(pp.sudogcache)
	s := pp.sudogcache[n-1]
	pp.sudogcache[n-1] = nil
	pp.sudogcache = pp.sudogcache[:n-1]
	if s.elem != nil {
		throw("acquireSudog: found s.elem != nil in cache")
	}
	releasem(mp)
	return s 
}

releasesudog

将sudog回收, 放回池子或者销毁
源码:

func releaseSudog(s *sudog) {
  // 回收的sudog必须的纯净的sudog, 所以在channel源码中, 每次唤醒sudog都会将sudog的参数清空
	if s.elem != nil {
		throw("runtime: sudog with non-nil elem")
	}
	if s.isSelect {
		throw("runtime: sudog with non-false isSelect")
	}
	if s.next != nil {
		throw("runtime: sudog with non-nil next")
	}
	if s.prev != nil {
		throw("runtime: sudog with non-nil prev")
	}
	if s.waitlink != nil {
		throw("runtime: sudog with non-nil waitlink")
	}
	if s.c != nil {
		throw("runtime: sudog with non-nil c")
	}
	gp := getg()
	if gp.param != nil {
		throw("runtime: releaseSudog with non-nil gp.param")
	}
	mp := acquirem()
	pp := mp.p.ptr() // 获取当前的p
	if len(pp.sudogcache) == cap(pp.sudogcache) { // 如果p的池子满了, 需要清理
		var first, last *sudog
		for len(pp.sudogcache) > cap(pp.sudogcache)/2 {
			n := len(pp.sudogcache)
			p := pp.sudogcache[n-1]
			pp.sudogcache[n-1] = nil
			pp.sudogcache = pp.sudogcache[:n-1]
			if first == nil {
				first = p
			} else {
				last.next = p
			}
			last = p
		}
		lock(&sched.sudoglock)
		last.next = sched.sudogcache
		sched.sudogcache = first
		unlock(&sched.sudoglock) // 原本sched中的所有sudog对象全部销毁, 从p中清理出一半的sudog对象给sched的池子
	}
	pp.sudogcache = append(pp.sudogcache, s) // 回收对象
	releasem(mp)
}

通过上面的流程可以清晰的知道, sched的池子最大数量是p的一半, 且sched的池子主要是用来销毁过剩的sudog和给p补充sudog对象的.