Go channel 分析(一)|Go主题月

535 阅读1分钟

基本用法

chan string          // 可以发送接收string
chan<- struct{}      // 只能发送struct{}
<-chan int           // 只能从chan接收int

这个箭头总是射向左边的,元素类型总在最右边。如果箭头指向 chan,就表示可以往 chan 中塞数据;如果箭头远离 chan,就表示 chan 会往外吐数据。

基本使用也就这 3 个,说点其他操作。

其他操作

  • 是忽略读取的值,只是清空 chan:
for range ch {}
  • send 和 recv 都可以作为 select 语句的 case clause:
func main() {
    var ch = make(chan int, 10)
    for i := 0; i < 10; i++ {
        select {
        // 向 chan push data
        case ch <- i:
        // 向 chan pull data,并赋值 =>v
        case v := <-ch:
            fmt.Println(v)
        }
    }
}

实现原理

从数据结构,初始化,以及 send, recv, close,三个惯常操作来看看 channel 的底层实现。

数据结构

chan 的数据结构如下图所示,数据类型是 runtime.hchan

type hchan struct {
    qcount   uint           // total data in the queue
    dataqsiz uint           // size of the circular queue
    buf      unsafe.Pointer // points to an array of dataqsiz elements
    elemsize uint16
    closed   uint32
    elemtype *_type // element type
    sendx    uint   // send index
    recvx    uint   // receive index
    recvq    waitq  // list of recv waiters
    sendq    waitq  // list of send waiters

    lock mutex
}
  • qcount:chan 中已经接收但还没被取走的元素的个数。值:len(ch)
  • dataqsiz:循环队列的大小。
  • buf:存放元素的循环队列的 buffer。
  • elemtype & elemsize:chan 中元素的 type 和 size(是元素本身的size)。
  • sendx:发送数据的指针在 buf 中的位置。
  • recvx:处理接收请求时的指针在 buf 中的位置。一旦取出数据,此指针会移动到下一个位置。
  • recvq:如果消费者因为没有数据可读而被阻塞了,就会被加入到 recvq 队列中。
  • sendq:如果生产者因为 buf 满了而阻塞,会被加入到 sendq 队列中。
  • lock:加锁能保护hchan的所有字段,包括waitq中sudoq对象。

初始化

Go 在编译的时候,会根据容量的大小选择调用 makechan64,还是 makechan。

关注 makechan 就好了,因为 makechan64 只是做了 size 检查,底层还是调用 makechan 实现的。 makechan 的目标就是生成 hchan 对象。

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

  // 略去检查代码
  mem, overflow := math.MulUintptr(elem.size, uintptr(size))

  //
  var c *hchan
  switch {
    case mem == 0:
    // chan的size或者元素的size是0,不必创建buf
    c = (*hchan)(mallocgc(hchanSize, nil, true))
    c.buf = c.raceaddr()
    case elem.ptrdata == 0:
    // 元素不是指针,分配一块连续的内存给hchan数据结构和buf
    c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
    // hchan数据结构后面紧接着就是buf
    c.buf = add(unsafe.Pointer(c), hchanSize)
    default:
    // 元素包含指针,那么单独分配buf
    c = new(hchan)
    c.buf = mallocgc(mem, elem, true)
  }

  // 元素大小、类型、容量都记录下来
  c.elemsize = uint16(elem.size)
  c.elemtype = elem
  c.dataqsiz = uint(size)
  lockInit(&c.lock, lockRankHchan)

  return c
}

最终,针对不同的容量和元素类型,这段代码分配了不同的对象来初始化 hchan 对象的字段,返回 hchan 对象。

send

Go 在编译发送数据给 chan 的时候,会把 send 语句转换成 chansend1 函数,chansend1 函数会调用 chansend:

send() -> chansend1() -> chansend()
func chansend1(c *hchan, elem unsafe.Pointer) {
    chansend(c, elem, true, getcallerpc())
}

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
  // 第一部分
  if c == nil {
    if !block {
      return false
    }
    gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
    throw("unreachable")		// L11
  }
  ......
}

第一部分: 如果 chan 是 nil 的话,就把调用者 goroutine park(阻塞休眠), 调用者就永远被阻塞住了,所以,第 11 行是不可能执行到的代码。

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
  ...
  // 第二部分,如果chan没有被close,并且chan满了,直接返回
  if !block && c.closed == 0 && full(c) {
    return false
  }
  ...
}

第二部分: 是当你往一个已经满了的 chan 示例发送数据时,并且想不阻塞当前调用,那么这里的逻辑是直接返回。chansend1 方法在调用 chansend 的时候设置了阻塞参数,所以不会执行到第二部分的分支里。