2.go channel源码-创建/关闭

152 阅读4分钟

上集回顾: channel结构体

1.创建

1.1 校验

创建时主要检验元素的大小、申请的内存大小和内存对齐等

/*
:parmas t: 存储了元素的类型,大小等
:params size: 期望大小
*/
func makechan(t *chantype, size int) *hchan {
    elem := t.elem

    // 编译器检查数据项大小不能超过 64kb
    if elem.size >= 1<<16 {
        throw("makechan: invalid channel element type")
    }
    // 检查内存对齐是否正确
    if hchanSize%maxAlign != 0 || elem.align > maxAlign {
        throw("makechan: bad alignment")
    }
    // 检查申请的内存大小,判断是否溢出
    mem, overflow := math.MulUintptr(elem.size, uintptr(size))
    if overflow || mem > maxAlloc-hchanSize || size < 0 {
        panic(plainError("makechan: size out of range"))
    }

   ...
}

1.2 内存分配

创建和初始化一个channel,分三种情况考虑了不同的情况:

  1. 当元素大小为0时,不需要为元素分配内存,此时创建channel只需要分配hchan结构体本身的内存即可,即mem==0。
  2. 当元素大小非零,但是元素没有指针字段时,直接将内存块追加到hchan结构体之后即可,这是因为没有指针字段意味着内存不需要被垃圾回收器扫描,这种情况下用hchan结构体和内存块一并分配更加高效。
  3. 最后一种情况是元素大小非零并且含有指针字段,所以我们需要为所有元素分配独立的内存空间,这样可以确保每个元素对应的指针被正确地扫描。

总之,这三种情况的分析是基于元素大小和是否包含指针字段这两个因素的,这些因素决定了我们应该如何分配内存。这样做有助于提高内存利用率和性能。

func makechan(t *chantype, size int) *hchan {
     
     ...
     
     // mem = 每个元素大小* 队列容量
 
    var c *hchan
    switch {
    case mem == 0:
        // 队列或者元素大小为0时,调用mallocgc()在堆上为channel开辟一段大小为 hchanSize 的内存空间。
        c = (*hchan)(mallocgc(hchanSize, nil, true))
        // 竞争检查器利用这个地址进行同步操作
        c.buf = c.raceaddr()
    case elem.ptrdata == 0:
        // 元素不包含指针:在一次调用中分配hchan和buf。
        // 调用 mallocgc()在堆上为channel和buf开辟一段连续的内存空间: hchanSize+ mem 
        c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
        c.buf = add(unsafe.Pointer(c), hchanSize)
    default:
        // 元素包含指针:hchan 单独开辟内存空间,缓冲区队列大小 = mem
        c = new(hchan)
        c.buf = mallocgc(mem, elem, true)
    }
    // channel中的元素的大小
    c.elemsize = uint16(elem.size)
    // channel中元素的类型信息
    c.elemtype = elem
    // 缓冲区队列容量大小
    c.dataqsiz = uint(size)
    // 在syncadjustsudogs() 中按锁定顺序获取了多个hchan
    lockInit(&c.lock, lockRankHchan)

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

2.关闭

2.1 校验

未初始化的channel不能关闭操作和已经关闭的channel不能重复关闭。最后做一些竞态检测。

func closechan(c *hchan) {
    if c == nil {
        panic(plainError("close of nil channel"))
    }

    lock(&c.lock)
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("close of closed channel"))
    }

    if raceenabled {
        callerpc := getcallerpc()
        racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan))
        racerelease(c.raceaddr())
    }

2.2 唤醒

关闭一个channel。关闭channel后,无法再向其中发送数据,但仍然可以从中接收数据,直到所有已发送的数据都被接收后,后续的接收操作会返回零值。

关闭channel会释放其中阻塞的发送和接收goroutine,并将它们唤醒,以便它们可以检查channel是否已经关闭并执行相应的关闭操作。这是为了避免goroutine永久地阻塞在channel上。

通过释放未被接收的数据,这样做可以确保所有的goroutine都被处理完毕,防止它们被永久地阻塞在channel上。

func closechan(c *hchan) {

    ...

    c.closed = 1

    // 新建glist,是一个队列结构(单向链表)
    var glist gList

    // 释放所有的接收者goroutine
    /*
    接收者通过释放`s.elem`来释放阻塞在通道上的goroutine所分配的内存空间。
    因为在发送者和接收者之间传递数据时,数据必须被拷贝到通道的内存中,
    而相应的goroutine将会阻塞直到数据被成功传递或者通道被关闭。
    如果没有释放这些内存空间,这些goroutine在阻塞期间分配的内存将不会被回收,可能会导致内存泄漏。
    */
    for {
        sg := c.recvq.dequeue()
        if sg == nil {
                break
        }
        if sg.elem != nil {
            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)
    }

    // 释放所有的发送者goroutine
   /*
       发送阻塞的goroutine是不需要释放任何内存的。相反,它要求是释放该goroutine所持有的值的引用(这个值被保存在`sg.elem字段中`)
   */
    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
        if raceenabled {
            raceacquireg(gp, c.raceaddr())
        }
        glist.push(gp)
    }
    unlock(&c.lock)

    // 我们已经解除频道锁定,准备好所有G。
    // 将阻塞状态的goroutine唤醒并使之处于就绪状态,以便它们可以继续执行。
    for !glist.empty() {
        gp := glist.pop()
        gp.schedlink = 0
        goready(gp, 3) // 唤醒goroutine
    }
}