Rust in Everything
基于 crossbeam-channel 0.5.17

API 与 flavor

主线位置:一条消息的入口。Sender::send 按 flavor 分发到 array::Channel::send,后面的事在 array 和 Context 与 Waker;channel 什么时候被释放由本页的 counter.rs 决定。

flowchart TB
  b0["bounded(0)"] --> zero["SenderFlavor::Zero"]
  bn["bounded(n), n > 0"] --> array["SenderFlavor::Array"]
  ub["unbounded()"] --> list["SenderFlavor::List"]
  af["after(d) / at(t)"] --> at["ReceiverFlavor::At"]
  tk["tick(d)"] --> tick["ReceiverFlavor::Tick"]
  nv["never()"] --> never["ReceiverFlavor::Never"]
  zero --> cnt["counter::Sender / Receiver"]
  array --> cnt
  list --> cnt
  cnt --> heap["一次 Box:Counter{senders, receivers, destroy, chan}"]
  at --> arc["Arc:只有接收端"]
  tick --> arc

构造函数与 flavor

公开 API 只有几个自由函数,每个都在 channel.rs 里,按参数挑一个 flavor:

函数flavor容量源码
unbounded()list无限channel.rs:45
bounded(0)zero0,会合channel.rs:109
bounded(n),n > 0arrayn同上
after(d)at1,只投递一次channel.rs:176
at(t)at1channel.rs:227
never()never0,永不就绪channel.rs:268
tick(d)tick1,周期投递channel.rs:327

bounded 的分支就是一个 if cap == 0:

pub fn bounded<T>(cap: usize) -> (Sender<T>, Receiver<T>) {
    if cap == 0 {
        let (s, r) = counter::new(flavors::zero::Channel::new());
        ...
    } else {
        let (s, r) = counter::new(flavors::array::Channel::with_capacity(cap));
        ...
    }
}

after、tick 在 Instant::now() + d 溢出时(比如传 Duration::MAX)退化成 never(),不 panic。never 从 0.5.16 起是 const fn(#1250),可以放进 static。

Sender 三种,Receiver 六种

SenderFlavor 只有 Array、List、Zero;ReceiverFlavor 多出 At、Tick、Never。后三种没有发送端:消息不是谁发来的,是接收时按当前时间「造」出来的,类型固定是 Receiver<Instant>(never 是任意 T,反正永远收不到)。所以 Receiver::recv 里 At / Tick 两支要用 mem::transmute_copy 把 Result<Instant, _> 转成 Result<T, _>:泛型函数里编译器不知道 T 就是 Instant,只能靠构造函数保证。

每个公开方法都是同一个形状:match &self.flavor,每个 flavor 调自己的同名方法。例如 Sender::send:

pub fn send(&self, msg: T) -> Result<(), SendError<T>> {
    match &self.flavor {
        SenderFlavor::Array(chan) => chan.send(msg, None),
        SenderFlavor::List(chan) => chan.send(msg, None),
        SenderFlavor::Zero(chan) => chan.send(msg, None),
    }
    .map_err(|err| match err {
        SendTimeoutError::Disconnected(msg) => SendError(msg),
        SendTimeoutError::Timeout(_) => unreachable!(),
    })
}

阻塞、带超时、带截止时间三种接口,在 flavor 里是同一个函数,区别只是 deadline 参数:send 传 None,send_timeout(d) 算出 now + d 再调 send_deadline,send_deadline 直接传 Some(deadline)。now + d 溢出时,send_timeout 退回不带超时的 send(0.5.7 的 #953「Improve handling of very large timeout」)。

flavor 的公共接口

每个存消息的 flavor 都实现同一组方法,没有 trait,靠 channel.rs 里的 match 约定:

方法作用
start_send(&mut Token) -> bool / start_recv预订一个槽:成功时把槽的位置写进 Token,返回 true;断开也返回 true,Token 里放空指针
write(&mut Token, msg) / read(&mut Token)按 Token 写入 / 读出,然后叫醒对面
try_send / try_recvstart_* 一次,不阻塞
send(msg, deadline) / recv(deadline)循环:start_*,不行就自旋、注册、park
len / capacity / is_empty / is_full / is_disconnected查询
disconnect*最后一个发送端或接收端 drop 时调

「预订」和「完成」分成两步,是为了 select:select! 先在多个 channel 上 start_*,选中一个后把 Token 交给调用方,调用方在分支里再 read / write。Token(select.rs:25)是一个结构体,每个 flavor 一个字段(at、array、list、never、tick、zero),谁用谁填。

counter.rs:谁来释放

array、list、zero 三种 flavor 的 channel 都包在 Counter 里,一次 Box 分配:

struct Counter<C> {
    senders: AtomicUsize,
    receivers: AtomicUsize,
    destroy: AtomicBool,
    chan: C,
}

counter::Sender<C> 和 counter::Receiver<C> 都只是一个裸指针。它没用 Arc,因为要分开数两边:channel 的「断开」由某一边的最后一个句柄触发,而释放内存要等两边都走光。

sequenceDiagram
  participant S as 最后一个 Sender
  participant C as Counter
  participant R as 最后一个 Receiver
  S->>C: senders.fetch_sub(1) == 1
  S->>C: disconnect(chan):置 mark 位,叫醒对面
  S->>C: destroy.swap(true) 返回 false:不释放
  R->>C: receivers.fetch_sub(1) == 1
  R->>C: disconnect(chan)
  R->>C: destroy.swap(true) 返回 true:Box::from_raw 释放

release:

pub(crate) unsafe fn release<F: FnOnce(&C) -> bool>(&self, disconnect: F) {
    if self.counter().senders.fetch_sub(1, Ordering::AcqRel) == 1 {
        disconnect(&self.counter().chan);

        if self.counter().destroy.swap(true, Ordering::AcqRel) {
            drop(Box::from_raw(self.counter));
        }
    }
}

destroy 是一个「第二个到的人负责」的标志:两边的最后一个句柄各 swap(true) 一次,拿到旧值 true 的那个是后到的,它释放。AcqRel 保证先到的一方在 disconnect 里做的写,对释放的一方可见。

acquire(clone 时调)计数超过 isize::MAX 就 process::abort()(counter.rs:48),防的是「克隆后 mem::forget」把计数绕回 0,和 Arc 的做法一样。

disconnect 闭包传哪个,决定了最后一个接收端 drop 时消息是否立即析构。Receiver 的 Drop:

ReceiverFlavor::Array(chan) => chan.release(|c| c.disconnect()),
ReceiverFlavor::List(chan) => chan.release(|c| c.disconnect_receivers()),
ReceiverFlavor::Zero(chan) => chan.release(|c| c.disconnect()),

list 有专门的 disconnect_receivers,会顺手丢弃缓冲里的消息;array 只有一个通用的 disconnect,消息要等 Counter 被释放、Channel 的 Drop 才析构。实验 E8 在 array 里。

at / tick / never 不进 Counter:前两者是 Arc<flavors::at::Channel>,never 是零大小类型,Drop 里什么也不做。

错误类型

err.rs 和 std 的 mpsc 一一对应,发送类错误都把消息还给调用方:

操作错误变体
sendSendError<T>(pub T)只有「断开」一种
try_sendTrySendError<T>Full(T)、Disconnected(T)
send_timeout / send_deadlineSendTimeoutError<T>Timeout(T)、Disconnected(T)
recvRecvError断开且已空
try_recvTryRecvErrorEmpty、Disconnected
recv_timeout / recv_deadlineRecvTimeoutErrorTimeout、Disconnected
Select::try_select 等TrySelectError、SelectTimeoutError、TryReadyError、ReadyTimeoutError单元结构体

接收侧的「断开」只在已经空了的时候报:发送端全走了,缓冲里还有的消息照样能收完。Receiver::iter() 就靠这个结束:recv 返回 Err 时迭代器返回 None。

超时了也可能成功

send_timeout 的文档在 0.5.17 补了一句(#1289,channel.rs:455):

If the channel is not full and the timeout has already elapsed, the message will still be sent.

recv_timeout / recv_deadline 也补了对称的一句(#1280、#1245):channel 非空时,即使截止时间已过,也返回下一条消息。

原因在 flavor 的循环顺序。array 的 send 每一轮先试 start_send,后看截止时间:

loop {
    let backoff = Backoff::new();
    loop {
        if self.start_send(token) {
            let res = unsafe { self.write(token, msg) };
            return res.map_err(SendTimeoutError::Disconnected);
        }
        if backoff.is_completed() { break; } else { backoff.snooze(); }
    }

    if let Some(d) = deadline {
        if Instant::now() >= d {
            return Err(SendTimeoutError::Timeout(msg));
        }
    }
    // 注册、park 到 deadline
}

所以 send_timeout(msg, Duration::ZERO) 不等于「保证不发」:有空位就发出去了。park_timeout 醒来以后也是先 start_send 再看时间,醒得晚了照样成功。想要「过了时间就别发」的语义,得在调用前自己看表;想要「不阻塞」,用 try_send。list 的 send 根本不看 deadline(无界,永远不满),zero 的 send 只要有接收方在等就立即配对。

❗IMPORTANT
动脑筋:bounded(1) 的 send_timeout(x, 0) 什么时候返回 Timeout?

channel 空着时调用,会返回什么?channel 已满、且没有任何接收方时呢?如果另一个线程在你调用前 1 微秒刚 recv 走一条,结果又是什么?

动脑筋答案

  • 空着:start_send 第一次就成功,返回 Ok(()),超时参数根本没被看。
  • 已满且没人收:内层循环自旋、snooze 到 is_completed()(约 11 次,见 Backoff),这期间满的状态不变;出来后 Instant::now() >= d,返回 Timeout(x),消息原样还回来。
  • 刚被收走一条:那一刻已经不满,第一次 start_send 就成功,返回 Ok(())。「超时」只在真的要阻塞时才会被检查。

小结

  • bounded(0) 是 zero、bounded(n) 是 array、unbounded() 是 list;after / at / tick / never 只有接收端,类型是 Receiver<Instant>。
  • 公开方法一律 match flavor 分发;阻塞、超时、截止时间三种接口在 flavor 里是同一个函数,只差 deadline。
  • 每个 flavor 把操作拆成 start_*(预订,结果写进 Token)和 read / write(完成),这是 select 能跨 channel 工作的前提。
  • Counter 分开数发送端和接收端,某一边走光就 disconnect,两边都走光时第二个到达的释放内存(destroy.swap(true))。
  • 最后一个接收端 drop 时,list 立即丢弃缓冲消息,array 和 zero 不会;0.5.17 的 array 要等发送端也走光。
  • 带超时的收发先尝试、后看表:超时参数为 0 也可能成功。

💬 关联讨论

发起讨论
加载中...