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
很可能会更快地被垃圾回收器回收。