Post

channel 原理

channel 原理

原理

channel 是 Go 中实现 goroutine 之间通信和同步 的核心机制。

底层数据结构是 hchan,主要由环形队列 + 锁 + 发送/接收队列 + 调度器组成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type hchan struct {
  qcount   uint      	// 队列中剩余元素数量
  dataqsiz uint     		// 循环队列的长度(channel的大小)
  buf      unsafe.Pointer // 长度为dataqsiz的底层数组指针,缓存型channel特有
  elemsize uint16		// 元素大小
  closed   uint32		// 是否关闭
  elemtype *_type 		// 接收、发送的的元素类型
  sendx    uint  		// 已发送元素在循环队列中的索引位置
  recvx    uint  		// 已接收元素在循环队列中的索引位置
  recvq    waitq  		// 接收者sudog等待队列(阻塞等待接收的goroutine)
  sendq    waitq  		// 发送者sudog等待对列(阻塞等待接收的goroutine)
  
  lock mutex				// 互斥锁
}
  • 环形队列: 当 chan 是有缓冲型,负责缓存数据。
  • 锁:并发控制
  • 发送/接收队列:保存发送/接收阻塞等待的 goroutine 。
  • 调度器:由 Go runtime 调度器完成

发送数据时,如果有等待的接收 goroutine,会直接将数据交给接收者;如果没有且缓冲区未满,则写入环形队列;如果缓冲区已满,则发送 goroutine 会进入 sendq 队列并被阻塞。

接收数据时,如果缓冲区有数据则直接读取;如果没有但有等待发送者,则直接接收数据;否则接收 goroutine 会进入 recvq 队列阻塞。

这里思考一个问题,那 goroutine1 和 goroutine2 又怎么互相知道自己的数据 ”到“ 了呢?

channel结构中的recvq、sendq保存着阻塞等待的goroutine,但goroutine1向环型队列中发送数据时,就会从recvp取出goroutine并唤醒。

channel OR mutex

  • 关注数据流动,考虑使用channel解决
  • 数据不流动,保护数据,使用mutex

关闭channel原则

The Channel Closing Principle:不要在接收端关闭channel,也不要关闭有多个并发发送者的channel

打破The Channel Closing Principle解决方案

  • panic与recover
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    func SafeSend(ch chan T, value T) (closed bool){
        defer func() {
            if recover() != nil {
                // the return result can be altered 
                // in a defer function call
                closed = true
            }
        }()
          
        ch <- value // panic if ch is closed
        return false // <=> closed = false; return
    }
    
  • sync.Once关闭channel
  • sync.Mutex关闭channel

channel panic 场景

  • 向已关闭的 channel 发送数据

  • 关闭已关闭的 channel

  • 关闭 nil channel

    1
    2
    3
    4
    
    func main() {
      var ch chan int // nil channel
      close(ch)      // panic: close of nil channel
    }
    

不会 panic 但需要注意的操作

  • 从已关闭的 channel 接收(不 panic,返回零值)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    ch := make(chan int, 1)
    ch <- 42
    close(ch)
    
    v1 := <-ch // 42(buffer 中的值)
    v2 := <-ch // 0(零值,channel 已空且已关闭)
    
    // 用 ok 判断是否真的有值
    v3, ok := <-ch // v3=0, ok=false
    
  • 向 nil channel 发送 / 从 nil channel 接收(永久阻塞,不 panic)
    1
    2
    3
    
    var ch chan int
    ch <- 1   // 永远阻塞(不是 panic)
    <-ch      // 永远阻塞(不是 panic)
    

参考

  • https://mp.weixin.qq.com/s/ZXYpfLNGyej0df2zXqfnHQ
  • https://www.cnblogs.com/-wenli/p/12710361.html
  • https://segmentfault.com/a/1190000019172554
This post is licensed under CC BY 4.0 by the author.