Skip to content

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 channel

6.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空且关闭已关闭有缓冲
发送永久阻塞panicpanic满则阻塞
接收永久阻塞返回零值返回零值空则阻塞
关闭panicpanicpanic—

核心原则:发送方关闭 channel,接收方判断 ok。永远不要在接收方关闭。


九、Channel 的工程实践与故障传播 ​

9.1 Channel 不只是通信工具,还是一种背压机制 ​

很多 Go 服务里,channel 真正重要的价值不是“语法优雅”,而是它天然把生产速度和消费速度绑定起来。

mermaid
flowchart LR
    P["producer"] --> C["channel buffer"]
    C --> W["worker / consumer"]
    W --> D["DB / RPC / 磁盘"]

当下游慢下来时,常见演化顺序是:

  1. worker 处理变慢
  2. channel buffer 被逐渐填满
  3. 发送方开始阻塞在 ch <- x
  4. 上游 goroutine 数上涨,甚至请求堆积
  5. 用户看到 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 / RWMutex
  • atomic
  • 分片锁
  • 专门的无锁队列 / 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"]
维度ChannelMutex
数据所有权传递所有权("这个数据现在是你的了")共享所有权(大家都能读写)
性能🟡 有小额锁开销✅ 极轻量(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 实现 ​

语言/框架实现方式对标
Gohchan + sudog 阻塞队列,语言内置原生 CSP
Rusttokio::sync::mpsc 或 std::sync::mpsc标准库/第三方 channel
Kotlinkotlinx.coroutines.channels.Channel协程库内置 CSP
Clojurecore.async 宏,Go-style channel直接移植 CSP
Pythonasyncio.Queue类似有缓冲 channel
C++Boost.Fiber channel 或自定义无标准实现

Go 的 hchan 与其他语言最大的不同是零中间拷贝:无缓冲 channel 的数据直接从发送者栈 → 接收者栈,不经过中间缓冲区。慢路径下 sudog 队列是通过 gopark/goready 直接挂起/唤醒 goroutine,而非忙等或条件变量。

参考:Go 源码 runtime/chan.go、CSP 论文

批注模式

💬 文章评论

暂无评论,来说点什么吧 👇

编程学习笔记