go channel 通道
一、底层实现
1、数据结构
type hchan struct {qcount uint // total data in the queuedataqsiz uint // size of the circular queuebuf unsafe.Pointer // points to an array of dataqsiz elementselemsize uint16closed uint32timer *timer // timer feeding this chanelemtype *_type // element typesendx uint // send indexrecvx uint // receive indexrecvq waitq // list of recv waiterssendq waitq // list of send waiters// lock protects all fields in hchan, as well as several// fields in sudogs blocked on this channel.//// Do not change another G's status while holding this lock// (in particular, do not ready a G), as this can deadlock// with stack shrinking.lock mutex
}type waitq struct {first *sudoglast *sudog
}
channel 用于 goroutine 之间通信和同步。主要由一个环形缓冲区(对于带缓冲的 channel)和两个指针(读指针和写指针)组成。每个 channel 还有一个锁(通常是自旋锁)来保证并发安全。
主要结构:环形缓冲区+读写指针+读写等待队列+锁
2、发送与接收
发送与接收是相对于协程而言的。
- 发送操作(
chan <- value)会检查channel是否已满:- 如果是无缓冲的
channel,发送会阻塞直到有接收操作。 - 如果是有缓冲的
channel,发送会阻塞直到有空间可用。
- 如果是无缓冲的
- 接收操作(
value := <-chan)会检查channel是否为空:- 如果是无缓冲的
channel,接收会阻塞直到有发送操作。 - 如果是有缓冲的
channel,接收会阻塞直到有数据可读
- 如果是无缓冲的
3、发送队列(sendq)和接收队列(recvq)
-
recvq 队列:当一个 goroutine 执行接收操作时,Go 调度器会检查 channel 的状态,接收的 goroutine 会被挂起,并加入到
recvq队列。 -
sendq 队列:当一个 goroutine 执行发送被阻塞时,发送的 goroutine 会被挂起,并加入到
sendq队列。
发送和接收队列是FIFO队列,阻塞线程按先进先出顺序被调度,活跃线程优先于阻塞队列中的线程被调度。
4、调度
调度器会负责管理协程的状态,协程被channel阻塞时进入等待队列,此时调度器可以将其他可运行的 goroutine 调度到 CPU 上。
二、内存管理:
1、内存分配
创建channel时分配内存,channel 内存的分配是通过内存分配器来完成的,它会根据需要为 channel 结构体和缓冲区(如果有)分配内存。
channel的结构:channel是一个指向chan类型的结构体,这个结构体包含了channel的基本信息(例如缓冲区大小、读写指针等)。- 缓冲区(如果是缓冲
channel):如果channel是缓冲的(即使用make(chan Type, size)创建的channel),Go 还需要为channel分配一个固定大小的缓冲区,以便存储数据。缓冲区的大小是channel类型的元素大小乘以缓冲区的长度。
2、内存回收
当一个 channel 被销毁或不再有任何引用时,它占用的内存会被垃圾回收器回收。
channel是引用类型,它本身是一个指针,指向一个底层的数据结构。这个底层结构体包含了与channel操作相关的数据,如缓冲区、队列、读写指针等- 由于
channel的底层数据结构需要在堆上进行管理,因此即使它在栈上有一个指针,实际的数据存储通常是在堆上,特别是当它的生命周期超过函数作用域时。 - 通过 逃逸分析,Go 运行时决定是否将
channel分配到栈上或堆上。如果channel在函数外部被使用(例如通过返回值或传递给其他协程),它会被分配到堆上。 - 如果
channel仅在一个函数内部,并且没有被返回或传递出去(即它的生命周期完全在栈帧内),Go 运行时可能会将它分配到栈上。这个优化是由 Go 运行时的逃逸分析(escape analysis)决定的。如果channel的引用没有逃逸出函数,它可能会分配在栈上;否则,它将分配在堆上。
3、回收时机
- 当
channel不再被引用时:如果channel变量超出了作用域,或者所有引用该channel的变量都被置为nil或销毁,垃圾回收器会将该channel标记为可回收对象。下一次 GC 执行时,它会回收这个channel占用的内存。 channel的缓冲区:如果channel是一个带缓冲的channel(make(chan T, N)),那么在回收channel结构本身时,缓冲区也会被回收。
4、内存分配和垃圾回收的优化
Go 的垃圾回收器会尽可能地减少对内存的管理开销,但当涉及到大量的 channel 操作时,频繁的内存分配和垃圾回收可能会对性能造成影响。为了减少这种影响,可以考虑以下优化:
- 复用
channel:如果可能的话,复用已经创建的channel,而不是每次都重新创建。 - 避免大缓冲区的
channel:如果channel的缓冲区过大,可能会占用大量内存,造成内存压力。使用适当大小的缓冲区可以减少内存消耗。 - 及时关闭
channel:在不再需要channel时,应该尽早关闭它,或者确保channel变量没有持续的引用。
channel 的关闭并不会直接触发垃圾回收,关闭 channel 只是告诉协程可以停止从该 channel 接收数据。在使用带缓冲区的 channel 时,关闭 channel 还意味着缓冲区中未处理的数据将无法再被写入。虽然关闭 channel 本身不影响垃圾回收的触发,但是关闭 channel 可以帮助协程更快地退出,从而可能减少内存泄漏的风险。如果一个 channel 被关闭且没有任何活跃的协程在使用它,那么这个 channel 很可能会更快地被垃圾回收器回收。
