Channel 面试题

1. 什么是 CSP?

CSP () 是一种并发编程模型,强调通过通信来共享内存,而不是通过共享内存而进行通信。Go 语言中的 Goroutine 和 Channel 就是 CSP 模型的实现。CSP 具有以下特点:

  • 避免共享内存:协程不直接修改变量,而是通过 Channel 进行通信
  • 天然同步:Channel 的发送/接收自带同步机制,无需手动加锁
  • 易于组合:Channel 可以嵌套使用,构建复杂并发模式

2. Channel 的底层实现原理是怎样的?

基于 hchan 结构体实现,主要包含三个核心组件:

  • 环形缓冲区
  • 两个等待队列 sendqrecvqsendq 存储因 channel 满而阻塞的发送者 goroutine,recvq 存储因 channel 为空而阻塞的等待接收的 goroutine
  • 互斥锁 mutex

hchan 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type hchan struct {
qcount uint // 当前缓冲区中的元素数量
dataqsiz uint // 环形缓冲区的大小
buf unsafe.Pointer // 指向环形缓冲区的指针

elemsize uint16 // 元素大小
closed uint32 // channel 是否已关闭
elemtype *rtype // 元素类型信息

sendx uint // 发送索引
recvx uint // 接收索引

recvq waitq // 等待接收的 goroutine 队列
sendq waitq // 等待发送的 goroutine 队列

lock mutex // 保护 hchan 结构体的互斥锁
}

hchan 结构体

3. 向 Channel 发送数据的过程是怎样的?

  1. 先检查是否有等待的接收者

    • 如果有:直接将数据发送给接收者,跳过缓冲区
    • 如果没有:尝试写入缓冲区
  2. 尝试写入缓冲区:

    • 如果缓冲区没满:将数据写入缓冲区,更新索引
    • 如果缓冲区已满:将发送者 goroutine 加入 sendq 队列,阻塞等待
  3. 被唤醒后继续执行:当有接收者从 channel 中读取数据时,会从 sendq 中唤醒一个等待的发送者,被唤醒的 goroutine 会完成数据发送并继续执行

4. 从 Channel 读取数据的过程是怎样的?

  1. 先检查是否有等待的发送者

    • 如果有:对于无缓冲 channel,直接从发送者那里接收数据;对于有缓冲 channel,先从缓冲区读取数据,然后把等待发送者的数据放入缓冲区
    • 如果没有:尝试从缓冲区读取数据
  2. 尝试从缓冲区读取数据:

    • 如果缓冲区不空:从缓冲区读取数据,更新索引
    • 如果缓冲区为空:将接收者 goroutine 加入 recvq 队列,阻塞等待

5. 从一个已关闭 Channel 仍能读出数据吗?

可以

只有当返回的 ok 为 false 时,读出的数据才是无效的

1
2
3
4
5
6
x, ok := <-ch
if !ok {
// channel 已关闭,x 是无效数据
} else {
// channel 仍然可用,x 是有效数据
}

6. Channel 在什么情况下会引起内存泄漏?

Channel 引起的内存泄漏一般是由于 goroutine 泄露

当 goroutine 被阻塞在 channel 上永远无法退出时,goroutine 本身和它的引用的所有变量都无法被 GC 回收,导致内存泄漏。比如一个 goroutine 在等待接收数据,但发送者已经退出了,goroutine 就会一直阻塞在接收操作上,无法退出。

或者是 select 语句使用不当,在没有 default 分支的 select 中,如果所有 case 都无法执行,select 就会一直阻塞,导致 goroutine 泄露。

7. 关闭 Channel 会产生异常吗?

试图重复关闭一个 channel、关闭一个 nil 值的 channel、关闭一个只有接收方的 channel 都会导致 panic 异常

8. 往一个关闭的 Channel 写入数据会发生什么?

直接 panic

9. 什么是 select?

核心作用是同时监听多个 channel 操作。

当有多个 channel 都可能有数据收发时,select 能够选择其中一个可执行的 case 进行操作,而不是按顺序逐个尝试。比如同时监听数据输入、超时信号、取消信号等。

10. select 的执行机制是怎样的?

随机执行。如果多个 case 同时满足条件,Go 会随机选择一个执行,这避免饥饿问题。

如果没有 case 能执行就会执行 default 分支,如果没有 default 分支就会阻塞等待,直到有 case 满足条件才会执行。

11. select 的实现原理是怎样的?

Go 运行时会将所有 case 进行随机排序,然后执行两轮扫描策略:

  1. 第一轮扫描:直接检查每个 channel 是否可读写,如果找到就绪的立即执行
  2. 如果都没就绪,第二轮就把当前 goroutine 加入到每个 case 对应的等待队列中,等待被唤醒执行

当某个 channel 变为可操作时,调度器会唤醒对应的等待的 goroutine,执行它的 case 逻辑。

其核心原理:case 随机化 + 双重循环检测

在默认的情况下,select 语句会在编译阶段经过如下过程的处理:

  1. 将所有的 case 转换成包含 Channel 以及类型等信息的 scase 结构体;
  2. 调用运行时函数 selectgo 获取被选择的 scase 结构体索引,如果当前的 scase 是一个接收数据的操作,还会返回一个指示当前 case 是否是接收的布尔值;
  3. 通过 for 循环生成一组 if 语句,在语句中判断自己是不是被选中的 case

scase 结构体定义如下:

1
2
3
4
5
6
7
type scase struct {
c *hchan // channel 指针
elem unsafe.Pointer // 发送/接收的数据指针
kind uint8 // case 的类型:发送、接收、default
pc uintptr // 程序计数器
releasetime int64 // 释放时间
}


参考资料: