Rust in Everything
基于 crossbeam-channel 0.5.17

和 tokio mpsc、flume 对照

主线位置:主线 1 的横向比较。同样是「一条消息从发送方到接收方」,crossbeam 让线程 park,tokio 让任务返回 Pending、由 Waker 叫醒;flume 两种都支持。

flowchart LR
  subgraph thread["线程阻塞"]
    cb["crossbeam:Context + park"]
    std["std mpsc:同一份移植"]
  end
  subgraph task["任务挂起"]
    tk["tokio mpsc:AtomicWaker + Pending"]
  end
  fl["flume:两种都有"]
  cb --- fl
  fl --- tk
  tk -->|"blocking_send / blocking_recv"| bo["在当前线程上 block_on"]

对照表

flume 只按公开 API 列,不读源码。

crossbeam-channel 0.5.17std mpsc(1.99.1)tokio mpsc 1.54.1flume 0.11.1
生产者 / 消费者MPMC,Receiver: CloneMPSC(内部是 MPMC,mpsc 不给 Receiver 实现 Clone)MPSCMPMC
等待方式thread::park / unpark(Context 与 Waker)同左任务返回 Pending,接收端一个 AtomicWaker,有界的发送端排在信号量的等待链表上同步和 async 两套接口
等待前自旋Backoff 自旋 + 让出约 11 次不自旋不自旋(任务直接让出)不读源码,不列
有界的背压环形缓冲,每槽一个 stamp(array)同左块链表 + batch_semaphore 许可(mpsc)有界 / 无界都有
无界的结构31 槽一块的链表,CAS 抢序号(list)同左32 槽一块,fetch_add 抢序号,读完的块复用—
零容量bounded(0),会合sync_channel(0)不支持(channel(0) panic)bounded(0)
selectselect! / Select,阻塞式无用 tokio::select! 组合 future有(Selector,需 select feature)
定时 channelafter / at / tick / never无用 tokio::time无
async无无只有 async;blocking_send / blocking_recv 给同步代码用send_async / recv_async
在 async 里阻塞会占住运行时的工作线程(tokio 卷阻塞在异步里)同左blocking_* 在运行时内调用直接 panic用 *_async 就不阻塞

tokio 的 blocking_recv 就是 crate::future::block_on(self.recv()):在当前线程上建一个停车器,把 recv() 这个 future 轮询到完成。所以它和 crossbeam 一样是「线程睡觉」,只是每次调用都要过一遍 future 的轮询和 Waker 的注册。在运行时的工作线程上调用会 panic(「Cannot block the current thread from within a runtime」),这是故意的。

吞吐(E6)

exp-crossbeam/channel-throughput/。每轮 200 万条 usize(zero 为 20 万条),生产者均分;1P1C、4P1C 由主线程接收,4P4C 再起 3 个接收线程。每个进程 5 遍取中位数,整张表在不同时段跑 3 次,下表是 3 次中位数的中位数(单位 Mmsg/s)。release,M2 Max 8P + 4E,macOS 不能绑核;测量前 1 分钟负载约 3,见末尾。

实现1P1C4P1C4P4C
crossbeam bounded(1024)35.215.929.4
crossbeam unbounded()71.158.657.8
crossbeam bounded(0)0.720.300.22
std sync_channel(1024)101.319.8—
std channel()74.910.0—
std sync_channel(0)0.730.30—
flume bounded(1024)28.27.713.8
flume unbounded()35.322.221.9
tokio async channel(1024)15.57.6—
tokio async unbounded_channel()34.36.8—
tokio blocking_*,channel(1024)10.75.3—
tokio blocking_recv,unbounded_channel()13.25.2—

三次整表测量开始前的 1 分钟负载分别是 2.93、2.47、2.95(表跑起来以后,实验自己的线程会把负载推到 4–9)。三次之间大多数格子差别在 10% 以内,波动最大的是 tokio blocking_* 的 1P1C(7.9 / 11.0 / 10.7 和 9.5 / 13.2 / 14.0)。

tokio 有两组,不是同一种比较:

  • tokio async:new_multi_thread().worker_threads(p + 1) 的运行时里,生产者和接收者都是任务,send().await / recv().await。这是 tokio mpsc 的本来用法,它和线程阻塞的 channel 比的是两种调度模型,包括运行时调度任务的开销。
  • tokio blocking_*:普通线程上 blocking_send / blocking_recv(无界的发送不会等,直接 send),形状和 crossbeam 那几行一样,但每次操作都要构造 future、在当前线程 block_on,这条路径本来就是给「同步代码偶尔要和 async 世界交换数据」用的,不是为吞吐设计的。

读这张表要注意:

  • 这是单一消息类型(usize)、空循环收发的极限吞吐,消费者什么也不做。消费者每条消息干一点活,channel 本身的开销占比就小得多,几种实现的差距会缩小。
  • 1P1C 有界的 std 远快于 crossbeam、4P1C 无界的 crossbeam 远快于 std,这两处在同源的代码之间,原因的一部分已经定位到具体源码行,见 vs-std;本页不再解释。
  • crossbeam 和 std 的 zero 都是 0.2–0.73,比有缓冲的低约 50 倍,原因在 zero。
  • 4P4C 只有 MPMC 的实现能测(crossbeam、flume)。

小结

  • crossbeam 和 std 是同一份代码的两个版本,阻塞都是 park;tokio 是任务模型,靠 Waker;flume 两种接口都有。
  • crossbeam 独有的是 select! 和定时 channel;std 什么都没加;tokio 用 tokio::select! 和 tokio::time 组合出同样的东西。
  • 本机极限吞吐上,crossbeam 的无界 channel 在多生产者时最快;有界 1P1C 是 std 最快;tokio 在运行时内的 async 收发比线程阻塞的实现低,blocking_* 更低,二者都不是同一种比较。
  • 所有数字只代表空消费者、usize 消息、本机这一种条件。

💬 关联讨论

发起讨论
加载中...