go channel

设计理念

在很多主流的编程语言中,多个线程传递数据的方式一般都是共享内存,共享内存需要通过锁这种并发机制来解决数据冲突。虽然 Go 语言中也能使用共享内存加互斥锁进行通信,但是 Go 语言中深受 CSP (Communicating sequential processes,通信顺序进程)理论的影响,提出了自己的设计哲学

Do not communicate by sharing memory; instead, share memory by communicating. ——『不要通过共享内存来通信,我们应该使用通信来共享内存』

而 channel 正是 CSP 理论和这一设计理念的体现之一。

那为什么 Go 语言更倾向于使用通信的方式交换消息呢?原因有以下几点

  1. 高级抽象:使用通信来共享内存相比于直接使用共享内存和互斥锁是一种更高级的抽象,实际上,Go 语言中的 Channel 也是基于共享内存和锁实现的,Go 语言通过两者组合实现 Channel 提供了更好的封装,也让程序的逻辑更加清晰;
  2. 协程解耦:相比于共享内存,使用通信来共享内存使原来依赖同一个片内存的多个协程成为了消息的生产者和消费者,不需要自己手动处理资源的获取和释放。
  3. 协程竞争:协程通过 Channel 传递消息,由于 Channel 实现了 CSP 机制,消息在被发送前只由发送方协程访问,在发送之后仅可被唯一的接受者访问,因此使用通信来共享内存从设计上就天然避免了线程竞争和数据冲突的问题。

基本介绍

channel 类型

channel 类型有三种:

  • nil channel : 空通道,发送接收操作会阻塞,关闭操作会 panic,可以通过 make 转为下面两种类型
  • unbuffered channel : 有缓冲通道
  • buffered channel : 无缓冲通道

channel 操作

channel 包含创建、发送、接收和关闭四种操作,以 int 类型为例,操作方式如下

类型创建发送接收关闭
nil channelvar ch chan int || var ch (chan int) = nilch <- tt := <- ch || t, ok := <- chclose(ch)
unbuffered channelch := make(chan int)ch <- tt := <- ch || t, ok := <- chclose(ch)
buffered channelch := make(chan int, capacity)ch <- tt := <- ch || t, ok := <- chclose(ch)

操作小结

不同的 channel 类型和操作类型,对于 goroutine 的阻塞和 panic 情况也是不同的,这里创建可省略,channel 的创建自然不会引起 goroutine 阻塞或 panic,只看其它三种操作

对于 nil channel

  • 发送、接收会引起当前 goroutine 阻塞
  • 关闭操作会 panic

对于无缓冲 channel

  • 发送和接收操作需要一一配对,执行发送操作的 goroutine 会阻塞,直到另外一个 goroutine 对该 channel 执行接收操作;同样,执行接收操作的 goroutine 也会阻塞,直到另外一个 goroutine 对该 channel 执行发送操作
  • 对于关闭操作,channel 只能关闭一次,再次关闭会触发 panic;
  • 无缓冲 channel 关闭后,执行发送操作会触发 panic;执行接收则会立刻返回该 channel 元素类型的零值,但第二次再执行接收操作则会阻塞。

对于有缓冲 channel

  • 当 channel 缓冲未满时,执行发送操作不会阻塞,反之则阻塞
  • 当 channel 缓冲不为空,执行接收操作不会阻塞,反之则阻塞
  • 对于关闭操作,channel 只能关闭一次,再次关闭会触发 panic;
  • 有缓冲 channel 关闭后,执行发送操作会触发 panic;执行接收时,若 channel 缓冲未空,则正常返回缓冲中的元素,channel 缓冲为空时,则返回该 channel 元素类型的零值,但第二次再执行接收操作则会阻塞。

常用场景

1. channel 协程同步、通信、并发

  • 场景:goroutine 有同步、通信、并发需求

  • 示例:一个生产者生产数据、多个消费者消费数据

    func producer(ch chan int, msgCount int) {
    	fmt.Println("start producing")
    	for i := 0; i < msgCount; i++ {
    		ch <- i 
    	}
    	close(ch)
    	fmt.Println("finish producing")
    }
    
    func consumer(v int) {
      fmt.Printf("consuming data: %d \n", v)
    	time.Sleep(time.Second)
    }
    
    func main() {
      // 并发:设置缓冲区为 5,最多可以并发 5 个consumer 同时处理
    	ch := make(chan int, 5)
    	go producer(ch, 100)
    	for ;; {
        // 同步:等待生产者生产消息后才启动新的 consumer goroutine
        v, ok := <- ch 
        if !ok {
          break
        }
        // 通信:通过 channel 将生产者的消息(v)传给 consumer
        go consumer(v)
      }
    	time.Sleep(time.Hour)
    }
    

2. for range 读取 channel

  • 场景:当需要不断从 channel 读取数据时;

  • 表现:当 channel 关闭时,for 循环会自动退出,无需主动监测 channel 是否关闭,可以防止读取已经关闭的channel。当 channel 缓冲为空时,当前 goroutine 自动阻塞在 for 语句,等待 channel 缓冲区有数据可读。

  • 示例:

    func producer(ch chan int) {
    	fmt.Println("start producing")
    	for i := 0; i < 10; i++ {
    		ch <- i
    	}
    	close(ch)
    	fmt.Println("finish producing")
    }
    
    func consumer(ch chan int) {
    	fmt.Println("start consuming")
      // for range 读取 channel
    	for v := range ch {
        fmt.Printf("consuming data: %d \n", v)
    	}
    	fmt.Println("finish consuming")
    }
    
    func main() {
    	ch := make(chan int, 5)
    	go producer(ch)
    	go consumer(ch)
    	time.Sleep(time.Second)
    }
    

3. select 处理多个 channel

  • 场景:需要对多个通道进行同时处理,但只处理最先发生的 channel 时;

  • 表现:select同时监控多个通道,随机处理未阻塞的 case。

  • 示例:通过 select-channel 实现超时控制

    func producer(ch chan int) {
    	fmt.Println("start producing")
    	for i := 0; i < 10; i++ {
    		ch <- i
    	}
    	close(ch)
    	fmt.Println("finish producing")
    }
    
    func consumer(ch chan int) {
    	fmt.Println("start consuming")
    	// 设置每个数据消费时间不能超过 1s
    	timer := time.NewTimer(time.Second)
    	defer timer.Stop()
    	for x := range ch {
    		done := make(chan bool)
        // 开启 goroutine 模拟消费过程
    		go func(v int) {
    			fmt.Printf("consuming data: %d \n", v)
    			time.Sleep(time.Second * 2) 
    			done <- true
    		}(x)
    		// select 处理多个 channel
    		select {
    		case <-timer.C:
    			fmt.Printf("comsuming %d timeout\n", x)
    		case <-done:
    			fmt.Printf("comsuming %d success\n", x)
    		}
    		timer.Reset(time.Second)
    	}
    	fmt.Println("finish consuming")
    }
    
    func main() {
    	ch := make(chan int, 5)
    	go producer(ch)
    	go consumer(ch)
    	time.Sleep(time.Second * 60)
    }
    

4. 使用channel的声明控制读写权限

  • 场景:goroutine 对某个通道只读或只写时

  • 目的:A. 使代码更易读、更易维护,B. 防止只读协程对通道进行写数据,但通道已关闭,造成panic。

  • 用法:

    • 如果协程对某个channel只有写操作,则这个channel声明为只写。
    • 如果协程对某个channel只有读操作,则这个channe声明为只读。
  • 示例

    // 生产者只进行写操作,返回只读 channel <- chan int,可禁止其它 goroutine 写,从而降低 bug 发生概率
    func producer(n int) <-chan int {
    	outCh := make(chan int, n)
    	go func() {
    		for i := 0; i < n; i++ {
    			outCh <- i
    		}
    		close(outCh)
    	}()
    	return outCh
    }
    
    // 消费者只进行读操作,参数声明为只读 channel <-chan int,可以防止消费者向inCh写数据
    func consumer(inCh <-chan int) {
    	for x := range inCh {
    		fmt.Println(x)
    	}
    }
    
    func main() {
    	inCh := producer(10)
    	consumer(inCh)
    }
    

源码分析

接下来,channel 源码分析分为数据结构、创建、发送和关闭四个部分,在深入 channel 源码之前,先了解一下预备知识

预备知识

MPG 模型

Go 语言通过 MPG 模型实现 goroutine 并发,其中,G 在源码中是一个以 g 命名的结构体,分析 channel 源码前建议阅读 Go MPG 模型与并发调度单元以对MPG 模型有一个比较全面的认识。

type g struct {
  // ...
  atomicstatus   uint32  // 表示 goroutine 的状态
  param          unsafe.Pointer // 唤醒时参数
  waiting        *sudog // 等待队列,后文会说到
  // ...
}
func getg() *g // 当前 goroutine 的 g 对象:
sudog 结构

g 对象中,有一个名字为 waiting的 sudog 指针,它表示这个 goroutine 正在等待什么东西或者正在等待哪些东西。这里展示几个与 channel 操作关联的字段和操作函数,如下

type sudog struct {
        // ....
        isSelect bool // 是否是 select 操作
        elem     unsafe.Pointer // 数据元素,channel 操作中会作为所发送或接收元素的存放位置    
        waitlink    *sudog // 等待链表,指向下一个等待的 goroutine
        c           *hchan // channel
}

func acquireSudog() *sudog {} //  申请一个 sudog 对象
func releaseSudog(s *sudog) {} // 释放一个 sudog 对象
gopark 和 goready

gopark 将当前的 goroutine 修改成等待状态,然后等待被唤醒

func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) 
goready

goready 将 goroutine 的状态修改为可运行状态,随后会被调度器运行。当被调度执行时,对应的 gopark 函数会返回。

func goready(gp *g, traceskip int) 
raceenabled

在编译时,使用 -race 参数,可以执行竞态检查,在我们即将要分析的源码中,有相当部分代码为 race 提供了支持。分析时会跳过这一部分,有兴趣的读者可以参考: https://blog.golang.org/race-detector

hchan — 数据结构

type hchan struct {
	qcount   uint           // channel 中的元素个数
	dataqsiz uint           // channel 中的循环队列的长度,即 buf 大小
	buf      unsafe.Pointer // 指向缓冲区 channel 元素的指针
	elemsize uint16 // 单个 channel 元素大小
	closed   uint32 // channel 关闭状态
	elemtype *_type // channel 元素类型
	sendx    uint   // 当前发送操作的索引
	recvx    uint   // 当前接收操作的索引
	recvq    waitq  // 因为对 channel 进行接收操作而等待的 goroutine 队列
	sendq    waitq  // 因为对 channel 进行发送操作而等待的 goroutine 队列
	lock mutex 			// hchan 互斥锁
}
// goroutine 等待队列,双向队列,遵循先进先出原则,实际数据结构是一个循环数组
type waitq struct {
	first *sudog
	last  *sudog
}

makechan — 创建

channel 创建最终会调用runtime.makechanhchan结构及其hchan.buf分配内存,runtime.makechan接收参数类型 t 和缓冲区大小 size 作为参数。

runtime.makechan主要做了两件事,一是检查待创建的 channel 所需要内存是否不超出限制,二是为 hchan 结构和元素的缓冲区分配内存,具体限制条件和内存分配情况如下

  • 单个 channel 元素大小不能超过 64KB
  • hchan.buf 所需内存不超过 ^uintptr(0),64bit 系统即 2^64 - 1
  • 传入 size 为 0,即创建的是 unbuffered channel,则只为runtime.hchan分配内存即可;
  • 传入 size 大于 0,即创建的是 buffered channel,且元素类型不是指针类型,则为runtime.hchanhchan.buf分配一块连续内存
  • 其它情况下会单独为runtime.hchanhchan.buf分配内存;
func makechan(t *chantype, size int) *hchan {
	elem := t.elem
	// channel 单个元素不能大于 64 KB,否则报错:channel element type too large (>64kB)
	if elem.size >= 1<<16 {
		throw("makechan: invalid channel element type")
	}
  // 检查对齐字节
	if hchanSize%maxAlign != 0 || elem.align > maxAlign {
		throw("makechan: bad alignment")
	}
	// 计算并检查 channel 元素需要的内存大小,不能超过 ^uintptr(0)
	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: 
		// channel 元素个数为 0 或者元素大小为 0,则为 hchan 结构分配内存即可
		c = (*hchan)(mallocgc(hchanSize, nil, true))
		c.buf = c.raceaddr()
	case elem.ptrdata == 0: 
		// channel 元素类型为指针,则为 hchan 结构体和`hchan.buf`分配一块连续内存
		c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
		c.buf = add(unsafe.Pointer(c), hchanSize)
	default: 
		// chan 元素类型不是指针,则为 hchan 结构体和`hchan.buf`单独分配内存
		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)
	return c
}

chansend — 发送

对 channel 执行发送操作时,底层会调用 runtime.chansend 函数。该函数接收四个参数:

  • c *hchan:被执行发送操作的 channel
  • ep unsafe.Pointer:待发送到 channel 的元素数据
  • block bool:是否阻塞;buffered channel 传入 false,unbuffered channel 或 nil channel 会传入 true;select 语句则传入 false;
  • callerpc uintptr:chansend 调用者的函数指针
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	if c == nil {
    // 向 nil channel 执行发送操作会调用 gopark 函数永久阻塞当前 goroutine
		if !block {
			return false
		}
		gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}
  
	if !block && c.closed == 0 && full(c) {
    // 非阻塞发送,channel 没有关闭但 buf 已满,发送失败。比如上层代码使用 select 向 channel 发送数据
		return false
	}
	
	lock(&c.lock)

	if c.closed != 0 {
    // 给已经关闭的 channel 发送会触发 panic
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}
	
	if sg := c.recvq.dequeue(); sg != nil {
    // 当前 channel 接收队列中有 goroutine 在等待,则调用 send 直接将数据发送给
    // 队首 goroutine,send 会将该 goroutine 标记为可运行状态,并放到当前 goroutine 
    // 所在 P 的 runnext 上,作为当前 P 的下一个可运行 goroutine
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}
  
	if c.qcount < c.dataqsiz {
		// channel 有 buf 且 buf 未满,则将元素放入缓冲区,更新 sendx、qcount,发送成功。需要说明
    // 的是, buf 是一个循环数组,所以当 sendx 等于 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
	}

	if !block {
    // 对于非阻塞发送,立刻返回,比如上层代码使用 select 向 channel 发送数据
		unlock(&c.lock)
		return false
	}

	// 对于阻塞发送,执行到这里说明发送条件不满足。此时会申请一个 sudog 对象 mysg,然后将 mysg 加入 channel 
  // 的发送等待队列的队尾,然后调用 gopark 使当前 goroutine 进入阻塞状态,等待 receiver goroutine 唤醒
	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
	c.sendq.enqueue(mysg)
	atomic.Store8(&gp.parkingOnChan, 1)
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)

  // 当前 goroutine 被 channel 的 receiver goroutine 唤醒,注意当前 goroutine 
  // 被唤醒时,元素 eq 已被 receiver 取走,因此不用将元素 eq 放入 buf 中
	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
}

现在,我们 sender goroutine 的角度来看,会发生三种情况:阻塞、不阻塞和 panic。总结如下

  1. 阻塞
    • block 为 true,channel 为 nil channel,发送操作会使 sender goroutine 永久阻塞
    • block 为 true,channel 为 unbuffered channel ,接收等待队列为空时会使 sender goroutine 阻塞
    • block 为 true,channel 为 buffered channel ,buf 已满时会使 sender goroutine 阻塞
  2. 非阻塞
    • block 为 true,channel 为 unbuffered channel ,接收等待队列不为空时不会引起 sender goroutine 阻塞
    • block 为 true,channel 为 buffered channel ,buf 未满时或接收等待队列不为空时都不会引起 sender goroutine 阻塞
    • block 为 false,不会引起 sender goroutine 阻塞
  3. panic
    • 对已经 close 的 channel 执行发送操作会引起 panic

需要说明的是,对 channel 执行发送操作,且 channel 的接收等待队列不为空时,会直接调用 runtime.send 直接将待发送元素发送给
队首 goroutine,并将 receiver goroutine 调度为当前 P 的下一个可运行 goroutine。而不是先发送到 buf 中,等待 receiver goroutine 来取,这也体现了 channel 使用通信来共享内存的设计理念。

另一点需要注意的是,sender goroutine 被唤醒执行后,会根据 mysg.success 判断channel 是否已关闭,若channel 已关闭,则会触发sender goroutine panic。

chanrecv — 接收

对 channel 执行接收操作时,底层会调用runtime.chanrecv函数。该函数接收三个参数:

  • c *hchan:被执行发送操作的 channel
  • ep unsafe.Pointer:接收到的channel 元素会放入该地址中,若传入 nil,表示忽略接收的元素
  • block bool:是否阻塞;buffered channel 传入 false,unbuffered channel 或 nil channel 会传入 true;select 语句则传入 false;

runtime.chanrecv源码分析如下

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	if c == nil {
		if !block {
      // 从 nil channel 非阻塞接收元素时直接返回,比如 select-case 执行 <- nil chan 操作
			return
		}
    // 从 nil channel 阻塞接收元素时将永久阻塞
		gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}

	if !block && empty(c) {
		// 非阻塞接收且 buf 为空,则给 eq 赋值为 c.elemtype 对应的零值,然后返回 
		if atomic.Load(&c.closed) == 0 {
			// channel 没有关闭,则可直接返回
			return false, false
		}
    // 进入if !block && empty(c) 分支后若刚好发送操作发生,则不会进入 if empty(c) 分支
		if empty(c) {
			if raceenabled {
				raceacquire(c.raceaddr())
			}
			if ep != nil {
				typedmemclr(c.elemtype, ep)
			}
			return true, false
		}
	}

	lock(&c.lock)
	if c.closed != 0 && c.qcount == 0 {
    // channel 已关闭,则给 eq 赋值为 c.elemtype 对应的零值,然后返回
		ifraceenabled {
			raceacquire(c.raceaddr())
		}
		unlock(&c.lock)
		if ep != nil {
			typedmemclr(c.elemtype, ep)
		}
		return true, false
	}

	if sg := c.sendq.dequeue(); sg != nil {
    // 当前 channel sendq 不为空,则调用 recv 函数处理接收操作
    // recv 处理一个 buf 为空或已满的 channel 的接收操作,并负责唤醒 sender goroutine,即 sg;
		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true, true
	}

	if c.qcount > 0 {
		// 当前 channel sendq 为空,但 buf 不为空,则从 buf 取出队首元素
		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
	}

	if !block {
    // 对于非阻塞接收,立刻返回,比如 上层代码使用 select 从 channel 接收元素
		unlock(&c.lock)
		return false, false
	}

  // 与阻塞发送操作类似,对于阻塞接收,执行到这里说明接收条件不满足。此时会申请一个 sudog 对象 mysg,
  // 然后将 mysg 加入 channel recvq 的队尾,然后调用 gopark 使当前 goroutine 进入阻塞状态,等待 
  // sender goroutine 唤醒并被调度执行
	gp := getg()
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
	mysg.elem = ep // 唤醒时元素拷贝到 mysg.elem,即相当于拷贝到 eq
	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)
	
	// 当前 receiver goroutine 被 sender goroutine 唤醒并被 P 调度执行
	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
}

现在,我们从 receiver goroutine 的角度来看,会发生阻塞和不阻塞两种情况,总结如下

  1. 阻塞
    • block 为 true,channel 为 nil channel,接收操作会使 sender goroutine 永久阻塞
    • block 为 true,channel 为 unbuffered channel ,发送等待队列为空时会使 receiver goroutine 阻塞
    • block 为 true,channel 为 buffered channel ,buf 为空时会使 receiver goroutine 阻塞
  2. 非阻塞
    • block 为 true,channel 为 unbuffered channel ,发送等待队列不为空时不会引起 receiver goroutine 阻塞
    • block 为 true,channel 为 buffered channel ,buf 未满时或发送等待队列不为空时都不会引起 receiver goroutine 阻塞
    • block 为 false,不会引起 receiver goroutine 阻塞

与 sendchan 不同的是,当 receiver goroutine 阻塞后被唤醒执行时,不会检查当前 channel 是否关闭

runtime.recv源码分析如下

// recv 处理一个 buf 为空或已满的 channel 的接收操作,并负责唤醒 sender goroutine,即 sg;
// 进入 recv 函数说明channel sendq 不为空,channel 接收操作一定成功。
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
	if c.dataqsiz == 0 {
    // 若为 unbuffered channel,则从 sender goroutine 即 sg 将所发送数据直接拷贝给 eq (eq不为 nil 的话)
		if ep != nil {
			recvDirect(c.elemtype, sg, ep)
		}
	} else {
		// 若 buf 已满,则从 buf 取出队首元素,然后将 sender goroutine 的元素数据放入 buf 队尾
		qp := chanbuf(c, c.recvx)
		if raceenabled {
			racenotify(c, c.recvx, nil)
			racenotify(c, c.recvx, sg)
		}
		if ep != nil {
      // 需要返回 channel 元素,将 buf 的队首元素拷贝给 eq
			typedmemmove(c.elemtype, ep, qp)
		}
		
		typedmemmove(c.elemtype, qp, sg.elem) 将 sender goroutine 的元素数据放入 buf 队尾
		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 // 取出 sender goroutine (sg 为 sudog, sg.g 为 sender goroutine 的 g 结构)
	unlockf()
	gp.param = unsafe.Pointer(sg)
	sg.success = true // 保证 sender goroutine 在阻塞期间即使 channel 被close 掉也不会引起 panic
	if sg.releasetime != 0 {
		sg.releasetime = cputicks()
	}
	goready(gp, skip+1) // 将 gp 置为可执行状态,并作为当前 P 的下一个可执行 goroutine
}

closechan — 关闭

对 channel 执行接收操作时,底层会调用runtime.closechan函数。runtime.chanrecv源码分析如下

func closechan(c *hchan) {
	if c == nil {
    // 对 nil channel 执行 close 会引起 panic
		panic(plainError("close of nil channel"))
	}

	lock(&c.lock)
	if c.closed != 0 {
    // 同一个 channel 重复 close 会引起 panic
		unlock(&c.lock)
		panic(plainError("close of closed channel"))
	}
	// 将关闭标志置为 1.
	c.closed = 1
  
	var glist gList
	// 唤醒 channel recvq 上的所有 receiver goroutine 
	for {
		sg := c.recvq.dequeue()
		if sg == nil {
			break
		}
		if sg.elem != nil {
      // typedmemclr 将 sg.elem 指向的内存区域置 0,即 recvq 的 receiver goroutine 都会接收到 c.elemtype 类型的零值
			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)
	}

	// 唤醒 channel sendq 上的所有 sender goroutine
	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 // 每个 sender goroutine 都会 panic,因为 sg.success 被置为了 false
		if raceenabled {
			raceacquireg(gp, c.raceaddr())
		}
		glist.push(gp)
	}
	unlock(&c.lock)

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

以上对 closechan 的源码分析中,有两种情况会引起当前 goroutine panic,一是关闭 nil channel,而是重复关闭非 nil channel。

另外一个需要注意的是,对于 sendq 和 recvq,closechan 的处理有一个必须关注的,那就是 sendq 不为空时,执行 closechan 后,sendq 的 sender goroutine 被唤醒调度执行时,会触发 panic,而 recvq 中的 receiver goroutine 则被唤醒执行后都会收到响应 channel 元素类型的零值。

小结

至此,本文介绍了 channel 的设计理念、基本操作常用方式,并从数据结构以及发送、接收和关闭四个方面做了源码分析。从设计理念来说,channel 就是为了 goroutine 通信需求而生的,这种通信或许只是信号传递,也可能是数据传递,而且,通过 hchan 的 sendq、recq以及 buf 这几个队列,实现通信顺序进程(Communicating sequential processes,CSP)模型。

参考

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值