Channel 底层实现
#golang · #Channel · #hchan · #环形队列 · #并发 · #底层实现 · #CSP · #Actor
Go 并发编程的基石——hchan 结构、环形队列、阻塞唤醒机制完整剖析。
一、Channel 的本质
1.1 hchan 运行时结构
go
// runtime/chan.go
type hchan struct {
qcount uint // 队列中元素个数
dataqsiz uint // 环形队列大小(cap)
buf unsafe.Pointer // 指向环形队列数组
elemsize uint16 // 元素大小
closed uint32 // 是否已关闭
elemtype *_type // 元素类型
sendx uint // 发送索引(写入位置)
recvx uint // 接收索引(读取位置)
recvq waitq // 接收等待队列(阻塞的 goroutine)
sendq waitq // 发送等待队列(阻塞的 goroutine)
lock mutex // 互斥锁
}
type waitq struct {
first *sudog // 等待队列头
last *sudog // 等待队列尾
}
type sudog struct {
g *g // 等待的 goroutine
elem unsafe.Pointer // 数据指针(发送/接收的值)
next *sudog // 链表下一个
prev *sudog // 链表上一个
// ...
}1.2 Channel 的类型
go
ch1 := make(chan int) // 无缓冲(同步)
ch2 := make(chan int, 10) // 有缓冲,cap=10
ch3 := make(chan<- int) // 只发送
ch4 := make(<-chan int) // 只接收二、环形队列
Channel 内部用环形队列实现缓冲:
sendx ─┐
↓
┌────┬────┬────┬────┬────┬────┐
│ a │ b │ c │ │ │ │ dataqsiz = 6
└────┴────┴────┴────┴────┴────┘
↑ ↑
recvx sendx 继续写入
qcount = 3 (当前有 a, b, c 三个元素)sendx:下一次发送写入的索引recvx:下一次接收读取的索引qcount:当前队列中元素个数- 队列为空时
qcount == 0;队列满时qcount == dataqsiz
2.1 为什么用环形缓冲区而非链表?——性能与内存的深度分析
Go channel 的缓冲区选择环形数组而非链表,这是一个精心的工程决策:
═══════════════════════════════════════════════════════════════
方案对比: 环形数组 vs 链表
═══════════════════════════════════════════════════════════════
维度 环形数组 (Go 的选择) 链表
─────────────────────────────────────────────────────────────
内存分配 make 时一次性分配 每次 send 分配一个节点
GC 压力 极低(一个数组对象) 高(每个元素一个对象)
cache 局部性 极好(连续内存) 差(节点分散在堆上)
入队/出队 O(1) 索引计算 O(1) 指针操作
内存开销/元素 elemsize(无额外开销) elemsize + 2×指针(16B)
容量限制 固定(make 时确定) 可动态增长
═══════════════════════════════════════════════════════════════
为什么 GC 压力是决定性因素?
═══════════════════════════════════════════════════════════════
Go 的 GC 是并发标记-清除,扫描成本 ∝ 堆上对象数量。
链表方案:
make(chan int, 1000) → 每次 send 分配一个 node
高频通信时: 每秒百万次 send → 每秒百万次 malloc + 百万个对象等待 GC
GC 扫描: 需要遍历所有 node 的指针 → 标记阶段变慢
量化: 1M msg/s × 24B/node (int + next + prev) = 24MB/s 的分配速率
→ GC 每秒需要回收 24MB → GOGC=100 时每 48MB 触发一次 GC
环形数组方案:
make(chan int, 1000) → 一次分配 1000×8B = 8KB 的数组
之后所有 send/recv 都是索引操作 → 零分配
GC 只需扫描 1 个对象(数组本身)
量化: 0 分配/s → GC 几乎不受 channel 通信影响 ✅
═══════════════════════════════════════════════════════════════
Cache 局部性的量化影响
═══════════════════════════════════════════════════════════════
环形数组:
buf 是连续内存 → 相邻元素在同一 cache line (64B)
对于 chan int (8B): 一个 cache line 装 8 个元素
→ 连续 8 次 recv 只需 1 次内存访问(后 7 次 L1 hit)
链表:
每个 node 分散在堆上 → 每次 recv 都是一次指针跳转
→ 每次跳转都可能 cache miss (~5ns L1 hit vs ~100ns L3 miss)
高频场景下的差距:
环形数组: 1M recv/s × 5ns = 5ms CPU 时间
链表: 1M recv/s × 50ns(平均) = 50ms CPU 时间
→ 环形数组快 10× !
═══════════════════════════════════════════════════════════════
环形数组的"缺点"——为什么可以接受?
═══════════════════════════════════════════════════════════════
1. 容量固定:
make(chan int, 100) → 永远只能缓冲 100 个元素
→ 但这恰好是 channel 的设计哲学: 有限缓冲 = 背压机制
→ 如果需要无限缓冲 → 不应该用 channel,而是用队列
2. 空间可能浪费:
make(chan int, 1000) 但实际只用了 10 个 → 浪费 990×8B
→ 但 channel 的 buffer 通常很小(GOMAXPROCS 的 2-4 倍)
→ 实际浪费可忽略
3. 不支持动态扩容:
→ 这是有意为之: 固定容量让 "满了就阻塞" 的语义清晰
→ 动态扩容会破坏背压机制
═══════════════════════════════════════════════════════════════
环形队列的索引计算——为什么不用 % 而用 if?
═══════════════════════════════════════════════════════════════
// Go runtime 的实际代码:
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0 // 环形回绕
}
// 为什么不写成 c.sendx = (c.sendx + 1) % c.dataqsiz ?
// 因为 % 运算(除法指令)比 if + 赋值慢 3-5×
// 而 dataqsiz 不一定是 2 的幂(用户可以 make(chan int, 7))
// 所以不能用 & (dataqsiz-1) 的位运算优化
// if 分支在这里几乎总是"不跳转"(只有到末尾才跳转)
// → 分支预测准确率 > 99% → 实际代价接近 0三、发送流程 — chansend
3.1 源码核心逻辑
go
func chansend(c *hchan, ep unsafe.Pointer, block bool) bool {
lock(&c.lock)
// 1. channel 已关闭 → panic
if c.closed != 0 { unlock(&c.lock); panic("send on closed channel") }
// 2. 如果有等待的接收者 → 直接传给接收者
if sg := c.recvq.dequeue(); sg != nil {
send(c, sg, ep, func() { unlock(&c.lock) })
return true
}
// 3. 缓冲区有空间 → 写入环形队列
if c.qcount < c.dataqsiz {
qp := chanbuf(c, c.sendx)
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz { c.sendx = 0 } // 环形
c.qcount++
unlock(&c.lock)
return true
}
// 4. 缓冲区满了 → 需要阻塞
if !block { unlock(&c.lock); return false } // 非阻塞模式
// 5. 将 goroutine 加入 sendq 等待队列 → 挂起
gp := getg()
mysg := acquireSudog()
mysg.elem = ep
c.sendq.enqueue(mysg)
gopark(...) // 挂起当前 goroutine
// 被唤醒后继续...
}3.2 send 函数(直接传递,绕过缓冲区)
go
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func()) {
if sg.elem != nil {
typedmemmove(c.elemtype, sg.elem, ep) // 直接拷贝给接收者
}
gp := sg.g
sg.elem = nil
goready(gp) // 唤醒接收者
unlockf()
}关键:当有等待的接收者时,数据直接拷贝给接收者,不经过环形队列。这是"无缓冲 channel"和"有缓冲但有等待者"的快捷路径。
四、接收流程 — chanrecv
4.1 源码核心逻辑
go
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
lock(&c.lock)
// 1. channel 已关闭,且缓冲区为空 → 返回零值
if c.closed != 0 && c.qcount == 0 {
unlock(&c.lock)
if ep != nil { typedmemclr(c.elemtype, ep) }
return true, false // selected=true, received=false
}
// 2. 如果有等待的发送者 → 直接从发送者取
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) })
return true, true
}
// 3. 缓冲区有元素 → 从环形队列取
if c.qcount > 0 {
qp := chanbuf(c, c.recvx)
if ep != nil { typedmemmove(c.elemtype, ep, qp) }
typedmemclr(c.elemtype, qp)
c.recvx++
if c.recvx == c.dataqsiz { c.recvx = 0 }
c.qcount--
unlock(&c.lock)
return true, true
}
// 4. 缓冲区为空 → 需要阻塞
if !block { unlock(&c.lock); return false, false }
// 5. 将 goroutine 加入 recvq → 挂起
gp := getg()
mysg := acquireSudog()
mysg.elem = ep
c.recvq.enqueue(mysg)
gopark(...)
// 被唤醒后继续...
}4.2 recv 函数(直接从发送者取数据)
go
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func()) {
if c.dataqsiz == 0 { // 无缓冲
if ep != nil { typedmemmove(c.elemtype, ep, sg.elem) }
} else { // 有缓冲:发送者数据已在 buf 中,接收者从 buf 取,然后发送者补充
qp := chanbuf(c, c.recvx)
if ep != nil { typedmemmove(c.elemtype, ep, qp) }
typedmemmove(c.elemtype, qp, sg.elem) // 发送者的值补充到 buf
c.recvx++
c.sendx++
}
goready(sg.g) // 唤醒发送者
unlockf()
}五、无缓冲 vs 有缓冲
5.1 无缓冲(同步 channel)
ch := make(chan int)
发送方 ch <- x ────────┐
├──→ 必须同时就绪
接收方 x := <-ch ───────┘
流程:
1. send 先到 → 加入 sendq 等待 → gopark
2. recv 后到 → 从 sendq 取 → 直接拷贝数据 → goready 发送者5.2 有缓冲(异步 channel)
ch := make(chan int, 3)
发送方 → 环形队列 buf → 接收方
满则阻塞 sendq 空则阻塞 recvq六、关闭 Channel
6.1 close 语义
go
close(ch)
// 之后的行为:
<-ch // 零值(已关闭且排空)
v, ok := <-ch // v=零值, ok=false
ch <- x // panic: send on closed channel
close(ch) // panic: close of closed channel6.2 close 源码
go
func closechan(c *hchan) {
if c == nil { panic("close of nil channel") }
lock(&c.lock)
if c.closed != 0 { unlock(&c.lock); panic("close of closed channel") }
c.closed = 1
// 1. 释放所有接收等待者(返回零值)
for { sg := c.recvq.dequeue(); ... goready(sg.g) }
// 2. 释放所有发送等待者(panic!)
for { sg := c.sendq.dequeue(); ... goready(sg.g) }
// 发送者被唤醒后发现自己向已关闭的 channel 发送 → panic
unlock(&c.lock)
}关键:
close唤醒所有等待者。接收者收到零值;发送者panic。
七、Select 多路复用
7.1 随机选择
go
func selectgo(cases []scase) (int, bool) {
// 1. 将所有 channel 加锁(按地址排序,防止死锁)
// 2. 遍历所有 case,检查是否就绪
// 3. 如果有就绪的 case → 随机选一个
// 4. 都没有就绪 → gopark 等待,被唤醒后重新检查
}随机选择:防止饥饿,不会总是优先处理第一个就绪的 case。
7.2 Select 的性能陷阱
go
// ❌ 每次 select 都加锁所有 channel
select {
case <-ch1:
case <-ch2:
// ... 100 个 channel
}
// 锁排序 + 100 个 channel 的加锁开销很大八、Channel 关闭原则
| 操作 | nil channel | 空且关闭 | 已关闭 | 有缓冲 |
|---|---|---|---|---|
| 发送 | 永久阻塞 | panic | panic | 满则阻塞 |
| 接收 | 永久阻塞 | 返回零值 | 返回零值 | 空则阻塞 |
| 关闭 | panic | panic | panic | — |
核心原则:发送方关闭 channel,接收方判断
ok。永远不要在接收方关闭。
九、Channel 的工程实践与故障传播
9.1 Channel 不只是通信工具,还是一种背压机制
很多 Go 服务里,channel 真正重要的价值不是“语法优雅”,而是它天然把生产速度和消费速度绑定起来。
mermaid
flowchart LR
P["producer"] --> C["channel buffer"]
C --> W["worker / consumer"]
W --> D["DB / RPC / 磁盘"]当下游慢下来时,常见演化顺序是:
- worker 处理变慢
channelbuffer 被逐渐填满- 发送方开始阻塞在
ch <- x - 上游 goroutine 数上涨,甚至请求堆积
- 用户看到 RT 变高、超时、goroutine 爆炸
所以 channel 经常是故障传播链中的中间放大器:它本身不是根因,但会把下游变慢准确地传导回上游。
9.2 无缓冲 / 小缓冲 / 大缓冲到底怎么选
| 方案 | 优点 | 风险 | 适用场景 |
|---|---|---|---|
| 无缓冲 | 语义最强,同步明确 | 任一方慢就双向阻塞 | 强同步、严格交接 |
| 小缓冲 | 吸收瞬时抖动 | 配太小仍容易阻塞 | 常规 worker pool |
| 大缓冲 | 短期吞吐更平滑 | 容易掩盖堆积、拉高内存 | 突发流量明显、可接受排队 |
一个很重要的认知是:大缓冲不等于高性能。
它常常只是把“现在阻塞”改成“稍后在更深的队列里阻塞”,最后表现为:
- goroutine 不立刻卡住
- 但排队延迟悄悄拉长
- 内存占用升高
- 真正爆的时候更难定位根因
9.3 一个典型实战:ingress -> jobs chan -> workers -> MySQL
mermaid
sequenceDiagram
participant I as Ingress
participant C as jobs chan
participant W as Worker
participant M as MySQL
I->>C: 投递任务
C->>W: worker 取任务
W->>M: 执行 SQL
Note over M: 慢 SQL / 锁等待
M-->>W: 返回变慢
Note over W: worker 被占住
Note over C: 队列逐渐堆满
I->>C: 继续投递
Note over I: 发送阻塞 / 请求堆积 / 超时这类场景里,别只盯着 channel,而要顺着链路往下找:
- worker 数是否足够
- MySQL 是否慢
- 任务是否有超时与取消
- 是否缺少背压/限流/丢弃策略
9.4 goroutine 泄漏为什么常和 channel 绑定出现
常见泄漏模式:
- 接收方永久等不到数据
- 发送方永久等不到消费者
for range ch永不退出,因为没人 close- select 缺少
ctx.Done()或超时分支
go
func worker(ch <-chan Task) {
for t := range ch { // 如果 ch 永远不关,这里就永远挂着
process(t)
}
}泄漏的代价不只是 goroutine 数多一点,还包括:
- 栈内存增长
- 调度器负担增加
- 被 goroutine 持有的对象无法释放
- 可能连带 fd、连接、timer 一起泄漏
9.5 排障:怎么看是不是 channel 设计出了问题
| 现象 | 优先怀疑 | 常用方法 |
|---|---|---|
| goroutine 数持续上涨 | 发送/接收阻塞、泄漏 | goroutine dump、pprof |
| RT 越来越高但 CPU 不高 | 下游慢导致 channel 堆积 | trace、下游指标 |
| 内存上涨 | 大 buffer 或 goroutine 持有对象 | heap profile |
| 吞吐不稳定 | buffer 配置不当、worker 不均衡 | benchmark、trace |
一个常用排障顺序:
text
1. 看 goroutine dump:卡在 chansend 还是 chanrecv
2. 看 channel 后面的下游是不是慢了
3. 看是否缺少 ctx.Done / timeout / close 协议
4. 看 buffer 是不是只是掩盖堆积
5. 再决定改为限流、丢弃、扩 worker、还是换 Mutex/队列模型9.6 什么时候不该用 Channel
以下几类问题,channel 往往不是最优解:
- 只是保护一个共享计数器或小状态
- 需要大量随机访问和复合读改写
- 追求极致低开销同步
- 任务不是流式传递,而是共享状态一致性
这时更适合:
Mutex / RWMutexatomic- 分片锁
- 专门的无锁队列 / ring buffer
十、常见面试题
Q1: Channel 是引用类型吗?
是。
make(chan int)返回*hchan的指针副本。函数传递 channel 不会复制底层数据。
Q2: 如何优雅关闭 channel?
go
// 只有一个发送者:直接 close
close(ch)
// 多个发送者:用 sync.Once 或 context
var once sync.Once
once.Do(func() { close(ch) })
// 接收者通知退出:用另一个 done channel
go func() {
<-done
close(ch)
}()Q3: nil channel 有什么用?
在 select 中禁用某个 case。select 中 nil channel 永远不会就绪:
go
var ch chan int // nil
select {
case <-ch: // 永远不会匹配
default:
// 总是走这里
}Q4: 为什么无缓冲 channel 的发送者在接收者准备好之前不分配缓冲区?
设计选择。无缓冲 channel 强调同步语义——发送和接收必须同时发生,数据直接从发送栈拷贝到接收栈,零中间拷贝。
十一、Channel 四大并发模式
11.1 Fan-In(多路汇集)
多个输入 channel 合并为一个输出 channel:
go
func fanIn(ch1, ch2 <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for {
select {
case v, ok := <-ch1:
if !ok { ch1 = nil } else { out <- v }
case v, ok := <-ch2:
if !ok { ch2 = nil } else { out <- v }
}
if ch1 == nil && ch2 == nil { return }
}
}()
return out
}
// 使用场景:合并多个数据源(如多个 worker 的结果汇聚到一个消费者)11.2 Fan-Out(一对多分发)
一个输入 channel 广播到多个消费者:
go
func fanOut(in <-chan int, n int) []<-chan int {
workers := make([]<-chan int, n)
for i := 0; i < n; i++ {
ch := make(chan int)
workers[i] = ch
go func(out chan<- int) {
defer close(out)
for v := range in {
out <- v
}
}(ch)
}
return workers
}
// 使用场景:CPU密集型任务并行处理(每个 worker 一个 goroutine)11.3 Pipeline(流水线)
多个阶段串联,每阶段的输出是下一阶段的输入:
go
func pipeline() {
// Stage 1: 生成数据
gen := func(nums ...int) <-chan int {
out := make(chan int)
go func() { for _, n := range nums { out <- n }; close(out) }()
return out
}
// Stage 2: 平方
sq := func(in <-chan int) <-chan int {
out := make(chan int)
go func() { for n := range in { out <- n * n }; close(out) }()
return out
}
// 串联
for n := range sq(sq(gen(2, 3, 4))) {
fmt.Println(n) // 16, 81, 256
}
}Pipeline 的背压处理:当消费者慢于生产者时,有缓冲 channel 可以充当"暂存区":
go
// 背压控制:有缓冲 channel + 限速
func rateLimitedPipeline(in <-chan int, bufSize, rateLimit int) <-chan int {
out := make(chan int, bufSize) // 缓冲区缓冲瞬时流量
limiter := time.NewTicker(time.Second / time.Duration(rateLimit))
go func() {
defer close(out)
for v := range in {
<-limiter.C // 限速:每秒最多 rateLimit 个
out <- v
}
}()
return out
}11.4 Timeout/Or-Done(超时/取消控制)
go
// 模式1:select + time.After(适合单次超时)
func fetchWithTimeout(ctx context.Context, ch <-chan string) (string, error) {
select {
case result := <-ch:
return result, nil
case <-time.After(3 * time.Second):
return "", fmt.Errorf("timeout")
case <-ctx.Done():
return "", ctx.Err()
}
}
// 模式2:Or-Done — 合并多个 done channel(任意一个关闭则全部关闭)
func orDone(channels ...<-chan struct{}) <-chan struct{} {
switch len(channels) {
case 0: return nil
case 1: return channels[0]
}
orDoneCh := make(chan struct{})
go func() {
defer close(orDoneCh)
select {
case <-channels[0]:
case <-channels[1]:
case <-orDone(slice(channels[2:])...):
}
}()
return orDoneCh
}十二、Channel vs Mutex — 选型决策
Go 语言社区有一个著名的原则:"Don't communicate by sharing memory; share memory by communicating." 但这不是教条——不同场景需要不同工具:
mermaid
flowchart TD
Q["你要保护什么?"]
Q -->|"共享数据的正确性<br/>(计数器/缓存/状态)"| M["Mutex / RWMutex<br/>✅ 简单直接<br/>✅ 适合高频读写少量数据"]
Q -->|"goroutine 间的协调<br/>(执行顺序/信号/流水线)"| C["Channel<br/>✅ 传递数据所有权<br/>✅ 天然背压<br/>✅ 适合 SEDA/流水线架构"]
Q -->|"既要保护状态又要协调"| BOTH["两者结合<br/>Mutex 保护内部状态<br/>Channel 协调 goroutine"]| 维度 | Channel | Mutex |
|---|---|---|
| 数据所有权 | 传递所有权("这个数据现在是你的了") | 共享所有权(大家都能读写) |
| 性能 | 🟡 有小额锁开销 | ✅ 极轻量(atomic 则更快) |
| 背压 | ✅ 天然支持(满则阻塞) | ❌ 需自行实现 |
| 可组合性 | ✅ select 多路复用 | ❌ |
| 调试难度 | 🟡 goroutine 泄漏难排查 | ✅ 数据竞争易排查(go race) |
| 适用场景 | goroutine 通信、任务分发 | 保护共享变量 |
有缓冲 vs 无缓冲 Channel 选型
| 特性 | 无缓冲 (make chan T) | 有缓冲 (make chan T, N) |
|---|---|---|
| 语义 | 同步:发送方等接收方 | 异步:缓冲区满前不阻塞 |
| 吞吐 | 受限于较慢的一方 | 解耦收发速率,吞吐更高 |
| 延迟毛刺 | ❌ 任一慢则双向阻塞 | ✅ 缓冲区吸收短暂波动 |
| 内存 | 不占用额外内存 | 占用 N × sizeof(T) |
| 调试 | 更容易定位(同步点明确) | 可能隐藏 goroutine 泄漏 |
| 适用 | 你希望每步都同步确认 | 生产者/消费者速率不匹配 |
推荐:默认从无缓冲开始。发现性能瓶颈后用 benchmark 验证是否有必要加缓冲——而非凭直觉。缓冲大小通常取 GOMAXPROCS 的 2-4 倍。
十三、CSP vs Actor —— 并发编程两大流派
Go 的 channel 属于 CSP(Communicating Sequential Processes,Tony Hoare 1978),与之并列的是 Actor 模型(Carl Hewitt 1973)。
12.1 核心差异
CSP (Go Channel)
┌───────────────────────────────────────┐
│ 通信是"通道"——channel 是一等公民 │
│ 发送方必须知道 channel,接收方亦然 │
│ goroutine 匿名,channel 有名 │
│ "不通过共享内存通信" │
└───────────────────────────────────────┘
Actor Model (Erlang/Akka)
┌───────────────────────────────────────┐
│ 通信是"寻址"——发给 Actor 的 Mailbox │
│ 发送方只需知道接收 Actor 的地址(PID) │
│ Actor 有名,消息异步投递到 Mailbox │
│ "每个 Actor 顺序处理自己的消息" │
└───────────────────────────────────────┘12.2 详细对比
| 维度 | CSP (Go Channel) | Actor (Erlang/Elixir/Akka) |
|---|---|---|
| 通信方式 | 命名 channel,收发双方耦合到 channel | 寻址 Actor PID,一对一的 Mailbox |
| 消息语义 | 同步(无缓冲)或异步(有缓冲) | 异步投递到 Mailbox(🔥天然异步) |
| 选择性接收 | select 多路复用 | receive 模式匹配(更强大) |
| 错误处理 | goroutine panic → 进程崩溃 | "Let it crash" 哲学,Supervisor 树自动重启 |
| 分布式 | ❌ 不支持(channel 是本进程的) | ✅ 天然支持(Actor 位置透明,可跨节点) |
| 背压 | 有缓冲 channel 满了自然背压 | 需要手动实现(Mailbox 大小限制等) |
| 编程模型 | 同步风格(goroutine+channel 像同步代码) | 消息驱动(Actor 被动响应消息) |
12.3 各语言 Channel 实现
| 语言/框架 | 实现方式 | 对标 |
|---|---|---|
| Go | hchan + sudog 阻塞队列,语言内置 | 原生 CSP |
| Rust | tokio::sync::mpsc 或 std::sync::mpsc | 标准库/第三方 channel |
| Kotlin | kotlinx.coroutines.channels.Channel | 协程库内置 CSP |
| Clojure | core.async 宏,Go-style channel | 直接移植 CSP |
| Python | asyncio.Queue | 类似有缓冲 channel |
| C++ | Boost.Fiber channel 或自定义 | 无标准实现 |
Go 的 hchan 与其他语言最大的不同是零中间拷贝:无缓冲 channel 的数据直接从发送者栈 → 接收者栈,不经过中间缓冲区。慢路径下 sudog 队列是通过
gopark/goready直接挂起/唤醒 goroutine,而非忙等或条件变量。
登录后即可发表评论 👇