3.go channel源码-发送/接收

126 阅读9分钟

上集回顾:channel创建/关闭

1.发送

为了你有更好的阅读体验,建议你先观看 channel结构体

我们需要有一个认知,接收方阻塞,其中缓冲区和sendq队列一定是空的。非阻塞状态,一是缓冲区未满,二是缓冲区已满,将发送方加入到sendq队列。

1.1 校验

校验部分你能看到如果非阻塞状态遇到未初始化、关闭的、缓冲区满等条件都会直接退出,返回false。

其中需要注意的是不能向已关闭的channel发送数据,会异常退出的

最后所有写入的数据都是需要加锁的,channel并发的保证。

还有一些额外的cpu时钟周期和竞态检测器,你会在这里找到答案:CPU时钟周期,以及竞态检测器

/*
:params c: hchan实例
:params ep: 指向发送缓冲区的指针
:params block: 是否加锁
:params cellerpc: 返回其调用者的程序计数器(PC)
*/
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {

    if c == nil {
        if !block {
                return false
        }
        // chanenl为空,channel无法发送数据,将G挂起,异常退出
        gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }

    if debugChan {
        print("chansend: chan=", c, "\n")
    }
    
    if raceenabled {
        racereadpc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(chansend))
    }

    // 一个已经关闭的channel是不可能从“准备发送”的状态变成“未准备好发送”的状态。
    // 所以在检查完channel是否关闭以后,就算channel关闭了,也不影响此处检查的结果
    if !block && c.closed == 0 && full(c) {
        return false
    }
    

    var t0 int64
    if blockprofilerate > 0 {
        // 获取CPU时钟周期
        t0 = cputicks()
    }

    // 锁住 channel,并发安全
    lock(&c.lock)
    // 不能向一个已经close的channel发送数据
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("send on closed channel"))
    }
    
    ...
    
}

1.2 存在阻塞接收方且新增发送方

如果存在阻塞接收方,证明缓冲区是空的,sendq也是空的。所以新增的发送方,可以先直接找recvq队列进行匹配。

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

    ...

    if sg := c.recvq.dequeue(); sg != nil {
        // 找到一个等待的接收者,我们将要发送的值传递给接收器,绕过信道缓冲区(如果有的话)
        send(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true
    }
    
    ...
    
}

1.3 缓冲区未满

在缓冲区未满的时候,可以将元素放到缓冲区,并将发送指针+1。也许你已经注意到当发送指针位置==容量值,发送指针位置=0发送指针位置==容量值,发送指针位置=0,没错,缓冲区也被定义为环形队列。

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

    // 2.缓冲区:当缓冲区未满,那么通过移动环形队列缓存的指针来存储消息
    if c.qcount < c.dataqsiz {
        // 通道缓冲区中有可用空间。对要发送的元素进行排队。
        qp := chanbuf(c, c.sendx)
        if raceenabled {
            racenotify(c, c.sendx, nil)
        }
        // 将数据从 ep 处拷贝到 qp
        typedmemmove(c.elemtype, qp, ep)
        c.sendx++
        // 循环队列,如果发送游标值等于容量值,游标值归0
        if c.sendx == c.dataqsiz {
            c.sendx = 0
        }
        // 队列元素数量+1
        c.qcount++
        // 解锁
        unlock(&c.lock)
        return true
    }
    
    ...
}

1.4 缓冲区已满且仍有发送方(阻塞)

因为缓冲区已经满的缘故,我们还需要一个队列可以进行对元素的存放,将元素进行sudog结构体针对当前G的封装,然后存入 sendq的双向队列中。最后就是将当前G让出执行权

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

    // 由于还没有接收者,如果程序不阻塞,将直接退出
    if !block {
        unlock(&c.lock)
        return false
    }
    

    // Block on the channel. Some receiver will complete our operation for us.
    // 对通道加锁,某个接收器将为我们完成操作(通过sudog封装当前的G,并放入sendq双向链表)

    // 获取当前g
    gp := getg()
    // 调用acquireSudog()方法获取一个sudog,可能是新建的sudog,也有可能是从缓存中获取的。设置好sudog要发送的数据和状态。
    // 比如发送的channel、是否在select中和待发送数据的内存地址等等。
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }

    // 在分配elem和在 go.waiting上排队的mysg之间没有堆栈分割,在copystack可以找到它
    mysg.elem = ep  // ch <- ep
    mysg.waitlink = nil
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.waiting = mysg
    gp.param = nil
    // 将配置好的 sudog 加入待发送的等待队列
    c.sendq.enqueue(mysg)
    
    ...
    
}

1.5 goroutine进入休眠状态

实现 Goroutine 的等待功能,通过 park() 函数使 Goroutine 进入休眠状态等待 channel 操作完成。避免了 Goroutine 固定时间间隔内(超时机制)不停地查询 channel 状态,从而尽可能地减少了 Goroutine 的 CPU 占用率。

atomic.Store8 使用了原子操作,将一个字节设为非0值。 其可以将一个标志设置为当前goroutine正在通道操作中被阻塞。

gopark() 函操作将当前的goroutine暂停并释放它的g标识符(goid)。只有在通道操作被执行或取消时,该goroutine才会再次恢复并重新启动。

KeepAlive:确保ep所引用的对象在此函数返回之前不被垃圾回收器回收。这是为了避免在发送操作期间发生垃圾回收,导致数据丢失。

综上所述,该代码段的作用是将一个goroutine加入到等待发送的队列中,并暂停该goroutine,直到channel有数据可用或者channel关闭。

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    ...
    omic.Store8(&gp.parkingOnChan, 1)
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
    KeepAlive(ep)
    
}

gopark

字段对应值含义
unlockfchanparkcommit一个函数指针,表示将当前的 goroutine 添加到等待列表中,并且等待另一个 goroutine 完成相应的操作后再将这个 goroutine 唤醒。
lockunsafe.Pointer(&c.lock)是 channel 的锁,表示用于挂起/恢复 channel 的读写操作以及其它的同步操作。在这里,c 是一个指向 hchan 结构体的指针,表示一个无缓冲、双向的 channel 的描述符。
reasonwaitReasonChanSend表示当前 goroutine 阻塞的原因是正在发送数据到 channel。
traceEvtraceEvGoBlockSend表示对当前操作进行跟踪和记录,可以用于运行时调试和性能分析。
traceskip2表示当前 goroutine 应该被挂起,直到有其它 goroutine 从 channel 中接收到数据,并将这个 goroutine 从等待列表中唤醒,以继续发送操作。

KeepAlive

当前goroutine没有被唤醒,那么在KeepAlive()函数调用结束之后,如果ep所引用的对象已经不再被任何值所引用,那么这个对象就会被垃圾回收器回收。

但是在等待期间(在调用park()gopark()之间), ep所引用的对象至少被一个值所引用,也就是被goroutine所引用,因此不会被垃圾回收器回收。

当某些事件(如channel被关闭)发生时,其他的goroutine将被唤醒并从等待队列中移除,当被唤醒的goroutine重新获取运行时,其内部的代码会继续执行下去。在这种场景下,KeepAlive()函数的作用已经得到了充分保证。

总之,Go语言运行时系统会根据需要来唤醒阻塞的goroutine,保证阻塞期间所引用的对象不会被回收,从而确保程序的正确性。

1.6 goroutine唤醒

在channel中的g被唤醒有两种

  1. 当接收者成功从channel中读取到数据时,它会将这个值返回给调用者,并唤醒等待在该channel上的goroutine继续执行。
  2. channel关闭后,会唤醒所有的等待队列(发送者/接收者)

无论是哪一种情况都需要归还sudog对象。sudog对象是goroutine和channel之间的中介对象,它会在挂起goroutine的时候被创建并加入等待队列,然后唤醒goroutine的时候归还,这个过程确保了sudog对象的有效重用,避免了频繁的创建和销毁对象的开销。

在最后当唤醒的goroutine处于channel关闭状态,这就意味着是向一个关闭chanenl发数据,直接异常处理。所以在日常对channel的关闭,最好要在发送端来掌控。

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

    // sudog算是对g的一种封装,里面包含了g,要发送的数据以及相关的状态。
    // goroutine被唤醒后会完成channel的阻塞数据发送。发送完最后进行基本的参数检查,解除channel的绑定并释放sudog
    // someone woke us up.
    // 当goroutine唤醒以后,解除阻塞状态
    if mysg != gp.waiting {
        // G等待队列已经损坏
        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

    // 归还sudog对象
    releaseSudog(mysg)
    // 当前goroutine被唤醒后,如果发现channel已经被关闭了,则panic(也就是说不能向已经关闭的channel发送数据)
    if closed {
        if c.closed == 0 {
            throw("chansend: spurious wakeup")
        }
        panic(plainError("send on closed channel"))
    }
    return true
}

2.接收

2.1 校验

校验有两种, 一是非阻塞状态:chanenl是nil或者空,都可以直接退出了,最后将ep清空。 二是阻塞状态:channel是关闭了,可以直接退出了,最后将ep清空。

还有一些额外的cpu时钟周期和竞态检测器,你会在这里找到答案:CPU时钟周期,以及竞态检测器

/*
:params c: hchan结构体
:params ep: 指向接收缓冲区的指针
:params block: 是否阻塞
:return selected,received: select关键字返回的结果,接收者返回的结果
*/
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // Raceenabled:不需要检查ep,因为它总是在堆栈上,或者是由reflect分配的新内存。

    if debugChan {
        print("chanrecv: chan=", c, "\n")
    }
    // 从一个nil的channel接收数据将会永远阻塞,异常退出
    if c == nil {
        if !block {
            return
        }
        gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }


    // 快速路径:在未获取锁的情况下检查失败的非阻塞操作
    if !block && empty(c) {

        // 原子操作,读取c.closed变量的值
        if atomic.Load(&c.closed) == 0 {
            // channel的关闭时不可逆的,所以能通过这种方式判定退出(空数据且通道关闭)
            return
        }
        
        if empty(c) {
            if raceenabled {
                raceacquire(c.raceaddr())
            }
            if ep != nil {
                // 根据类型清理相应地址的内存
                typedmemclr(c.elemtype, ep)
            }
            return true, false
        }
    }
 
    var t0 int64 
    if blockprofilerate > 0{ 
    t0 = cputicks() 
    }    
    
    
    lock(&c.lock)
    
    if c.closed != 0 {
        // 通道已关闭且没数据
        if c.qcount == 0 {
            // 通道不存在元素,直接收尾
            if raceenabled {
                raceacquire(c.raceaddr())
            }
            unlock(&c.lock)
            if ep != nil {
                // 根据类型清理相应地址的内存
                typedmemclr(c.elemtype, ep)
            }
            // 从已关闭且没数据的 channel 接收,selected 会返回true,received返回 false
            return true, false
        }
    } else {
        ...
    }
    
    ...
    

2.2 存在阻塞发送方并新增接收方

如果存在阻塞发送方,证明缓冲区是满的,并且recvq是空的,所以新增的接收方,可以直接找recvq队列进行匹配。

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    
    ...
    
    if c.closed != 0 {
        ...
    } else {
        if sg := c.sendq.dequeue(); sg != nil {
            recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
            return true, true
        }
    }
 
    ...
 
}

2.3 缓冲区未满

当缓冲区未满的时候,可以将接收指针所在的元素赋值给接收者,然后移除缓冲区元素,并将数量-1。

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    
    ...
    
    if c.qcount > 0 {
        // 直接从队列中接收
        qp := chanbuf(c, c.recvx)
        if raceenabled {
            racenotify(c, c.recvx, nil)
        }
        // 将该数据复制到接收对象
        if ep != nil {
            typedmemmove(c.elemtype, ep, qp)
        }
        typedmemclr(c.elemtype, qp)
        c.recvx++
        if c.recvx == c.dataqsiz {
            c.recvx = 0
        }
        // 队列元素数量-1
        c.qcount--
        unlock(&c.lock)
        return true, true
    }

    if !block {
        unlock(&c.lock)
        return false, false
    }
    
    ...
}

2.4 缓冲区已空且仍有接收方

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    
    ...
    
    // 通过sudog封装当前G,并加入到recvq双向链表中
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }
    
    // 在分配elem和在gp上排队mysg之间没有堆栈割裂。在拷贝堆能找到的地方等待
    mysg.elem = ep
    mysg.waitlink = nil
    gp.waiting = mysg
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.param = nil
    // 加入recvq等待队列
    c.recvq.enqueue(mysg)

    ...

}

2.5 goroutine进入休眠状态

关于下边的信息参考:章节(1.5)

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
   
    ...
    
    atomic.Store8(&gp.parkingOnChan, 1)
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
    ...

gopark

字段对应值含义
unlockfchanparkcommit一个函数指针,表示将当前的 goroutine 添加到等待列表中,并且等待另一个 goroutine 完成相应的操作后再将这个 goroutine 唤醒。
lockunsafe.Pointer(&c.lock)是 channel 的锁,表示用于挂起/恢复 channel 的读写操作以及其它的同步操作。在这里,c 是一个指向 hchan 结构体的指针,表示一个无缓冲、双向的 channel 的描述符。
reasonwaitReasonChanReceive表示当前 goroutine 阻塞的原因是channel正在接收
traceEvtraceEvGoBlockRecv表示对当前操作进行跟踪和记录,可以用于运行时调试和性能分析。
traceskip2表示当前 goroutine 应该被挂起,直到有其它 goroutine 从 channel 中接收到数据,并将这个 goroutine 从等待列表中唤醒,以继续发送操作。

2.5 唤醒

同样,当发送者成功将数据发送到channel时,它会将这个值返回给调用者,goroutine被唤醒后会完成channel的阻塞数据接收.接收完最后进行基本的参数检查,解除channel的绑定并释放sudog。

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    
    ...
    
    // 
    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
}

3.公共函数

3.1 数据发送

这个函数将发送数据到一个通道,并调度等待数据的协程。同时,如果启用了竞争检查,则会进行数据竞争监视。

在发送操作中,发送者将要发送的值ep被复制到接收者sg中,然后接收者会被唤醒并能够继续运行。

在执行发送操作之前,通道c必须是空的并且已被锁定。发送完毕后,通道c将被解锁并使用unlockf进行解锁。

同时,接收者的sudog必须已经从该通道中出队,并且发送的值ep必须是非空的,指向堆或调用者的堆栈。

/*
:params c: 要接收数据的channel
:params sg: 当前select 或 go语句对应的 sudog结构体
:params ep: 接收到的数据要存储的位置
:params unlockf: 用于解锁channel,通常是一个函数类型
:params skip: 表示是否跳过少量的接收操作(因为channel中可能同时存在多个接收者)
*/
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    if raceenabled {
        if c.dataqsiz == 0 {
            racesync(c, sg)
        } else {
            // 虽然我们直接复制,但假装我们经历了缓冲区。需要注意的是,只有在启用了"raceenabled"情况下才需要增加头/尾位置。
            racenotify(c, c.recvx, nil)
            racenotify(c, c.recvx, sg)
            c.recvx++
            if c.recvx == c.dataqsiz {
                    c.recvx = 0
            }
            c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
        }
    }
    
    if sg.elem != nil {
        // 将要发送的数据ep复制到接收者sg中(直接拷贝内存(从发送者到接收者))
        sg.elem = nil
        sendDirect(c.elemtype, sg, ep)
    }
    gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
    sg.success = true
    if sg.releasetime != 0 {
        sg.releasetime = cputicks()
    }
    // 将G重新放入处理器p的本地运行队列,等待被调度
    goready(gp, skip+1)
}

3.2 数据接收

当通道已满时,接收任务(recv)在通道上执行接收操作。包括以下两个部分:

  1. 将发送者(sg)发送的值放入通道中,唤醒发送者并让其继续执行。
  2. 将接收者(当前的G)接收到的值写入指针变量ep所指向的位置。

对于同步通道,这两个值是相同的;对于异步通道,接收者从通道缓冲区获取数据,而发送者的数据则被放入通道缓冲区中。在执行接收操作时,需要确保通道已满且已被锁定,此时使用解锁函数(unlockf)解锁通道。接收时,指针变量ep必须非空,且必须指向堆或调用者的堆栈上的位置。

/*
:params c: 要接收数据的channel
:params sg: 当前select 或 go语句对应的 sudog结构体
:params ep: 接收到的数据要存储的位置
:params unlockf: 用于解锁channel,通常是一个函数类型
:params skip: 表示是否跳过少量的接收操作(因为channel中可能同时存在多个接收者)
*/
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    if c.dataqsiz == 0 {
        if raceenabled {
            racesync(c, sg)
        }
        if ep != nil {
            // copy data from sender
            // 从 sender里面拷贝数据
            recvDirect(c.elemtype, sg, ep)
        }
    } else {
        // 如果队列已经满了,就需要将队列头部的元素弹出,让发送者将它的元素放到队列尾部。
        // 因为队列已经满了,所以这两个位置是相同的。
        qp := chanbuf(c, c.recvx)
        if raceenabled {
            racenotify(c, c.recvx, nil)
            racenotify(c, c.recvx, sg)
        }

        // 将数据从buf中拷贝到接收者内存地址中
        if ep != nil {
            typedmemmove(c.elemtype, ep, qp)
        }

        // 将数据从sendq中拷贝到buf中
        typedmemmove(c.elemtype, qp, sg.elem)
        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
    unlockf()
    gp.param = unsafe.Pointer(sg)
    sg.success = true
    if sg.releasetime != 0 {
        sg.releasetime = cputicks()
    }
    goready(gp, skip+1)
}