Rust in Everything
基于 tokio 1.54.1

oneshot、watch、broadcast、Notify

主线位置:任务主线的「poll 返回 Pending → 被唤醒」。这四个原语各自用一个专门的数据结构决定「叫醒谁、叫醒几个、错过了怎么办」。

flowchart TB
  q{要传什么} -->|一个值,一次| os[oneshot]
  q -->|状态,只关心最新| w[watch]
  q -->|每条消息,每个接收者都要| bc[broadcast]
  q -->|一串消息,一个接收者| mpsc[mpsc]
  q -->|不传数据,只是叫醒| n[Notify]
  w -.内部用.-> n
  n --> one["notify_one:存 1 个许可"]
  n --> all["notify_waiters:不存"]

oneshot:一次分配,一个状态字

整个 channel 是一个 Arc<Inner<T>>,Sender 和 Receiver 各持有一个引用,只有一次堆分配(Inner):

struct Inner<T> {
    /// Manages the state of the inner cell.
    state: AtomicUsize,

    /// The value. This is set by `Sender` and read by `Receiver`. The state of
    /// the cell is tracked by `state`.
    value: UnsafeCell<Option<T>>,

    /// The task to notify when the receiver drops without consuming the value.
    // ...
    tx_task: Task,

    /// The task to notify when the value is sent.
    // ...
    rx_task: Task,
}

struct Task(UnsafeCell<MaybeUninit<Waker>>);

两个 waker 槽是 MaybeUninit<Waker>,里面有没有东西由状态字里的位记录(第 1488 行):

位常量谁设置含义
0b00001RX_TASK_SET接收端rx_task 里有接收端的 waker
0b00010VALUE_SENT发送端发送端结束了,value 归接收端
0b00100CLOSED接收端接收端不要这个值了
0b01000TX_TASK_SET发送端tx_task 里有发送端的 waker(Sender::closed() 在等)

value 本身不加锁,访问权由 VALUE_SENT 决定:置位前只有发送端能碰,置位后只有接收端能碰。

发送(Sender::send)先把值写进 value,再调 complete。complete 用 CAS 循环置 VALUE_SENT(set_complete),但如果已经 CLOSED 就不置:这时接收端已经走了,发送端把值从 value 里拿回来,作为 Err(t) 返回。置位成功且 RX_TASK_SET 已置,就叫醒接收端。注释解释了为什么这里是 CAS 而不是 fetch_or:关闭以后还置 VALUE_SENT,接收端会以为可以读 value,而发送端同时也在读它拿回值。

发送端没发就 drop(Drop for Sender)也调 complete。value 里是 None,接收端看到 VALUE_SENT 去取值,取出 None,返回 RecvError。「发送端消失」和「值已送达」走的是同一条路径。

接收(poll_recv)先扣预算,然后看状态:VALUE_SENT 就取值;CLOSED 就报错;否则把 waker 写进 rx_task、置 RX_TASK_SET,置位时返回的旧状态里如果已经有 VALUE_SENT,说明发送端刚好在这期间完成,直接取值。换了一个 waker(will_wake 为假)时,要先清掉 RX_TASK_SET 才能改写 rx_task,因为发送端只在看到这一位时才读它。

接收端 drop 或 close()(Inner::close)置 CLOSED,叫醒在 closed() 上等的发送端;如果值已经送到,Receiver 的 Drop 负责把它 drop 掉。

📝NOTE
没有蠢问题

问: 有界 mpsc 的 send 要 .await,oneshot 的 send 却是普通函数。它不怕满吗?

答: 它不可能满。存值的地方是 Inner 里那个 value: UnsafeCell<Option<T>>,channel 创建时就分配好了,容量正好是 1;而 send(mut self, t: T) 按值拿走 Sender,一个 Sender 只能发一次,第二次调用连编译都过不了。所以发送端永远有地方放值,没有什么需要等。唯一的失败是接收端已经走了(CLOSED),这时也不用等,complete 置位失败,值原样作为 Err(t) 退回。接收端什么时候来取,跟发送端无关。

watch:版本号加读写锁

Shared:

struct Shared<T> {
    /// The most recent value.
    value: RwLock<T>,

    /// The current version.
    // ...
    state: AtomicState,

    /// Tracks the number of `Receiver` instances.
    ref_count_rx: AtomicUsize,

    /// Tracks the number of `Sender` instances.
    ref_count_tx: AtomicUsize,

    /// Notifies waiting receivers that the value changed.
    notify_rx: big_notify::BigNotify,

    /// Notifies any task listening for `Receiver` dropped events.
    notify_tx: Notify,
}
  • value 外面是同步的 RwLock(loom::sync::RwLock,parking_lot 或 std 的),不是 tokio::sync::RwLock。borrow() 返回的 Ref 拿着读锁,文档提醒不要长时间持有它:持有期间发送端会被阻塞,在同一个线程上还可能死锁。
  • state 是一个 usize:第 0 位是关闭标志,版本号每次加 2(STEP_SIZE),这样加版本不会碰到关闭位。
  • 每个 Receiver 自己记着「上次看到的版本」。

send_if_modified 是所有发送方法的核心:拿写锁,调用户的闭包改值,闭包返回 true 就在持写锁时把版本加 2,放锁,最后 notify_rx.notify_waiters()。版本在锁内递增,注释说这样接收端才能知道自己读到的值对应哪个版本。

changed_impl:

loop {
    // In order to avoid a race condition, we first request a notification,
    // **then** check the current value's version. If a new version exists,
    // the notification request is dropped.
    let notified = shared.notify_rx.notified();

    if let Some(ret) = maybe_changed(shared, version) {
        return ret;
    }

    notified.await;
    // loop around again in case the wake-up was spurious
}

先创建 Notified,再比较版本。下面讲 Notify 时会看到,notified() 在创建时就记下了 notify_waiters 的调用次数,所以比较版本之后、await 之前发生的 send 不会被错过。changed() 外面套了 cooperative,会扣预算。

BigNotify:8 个 Notify

pub(super) struct BigNotify {
    #[cfg(not(all(not(loom), feature = "sync", any(feature = "rt", feature = "macros"))))]
    next: std::sync::atomic::AtomicUsize,
    inner: [Notify; 8],
}

BigNotify 把接收端分散到 8 个 Notify 上。每个 Notify 的等待链表有一把锁,一个 watch 有成百上千个接收端同时 changed() 时,它们都要往链表里挂节点,挤在一把锁上。分成 8 份,争用就少了。notified() 用线程本地随机数选一个(第 421 行),没有随机数源时轮流选;注释说简单测试里随机比轮流稍快,推测是因为轮流本身要争用 next 计数。notify_waiters 则 8 个全叫。

📝NOTE
冷知识:BigNotify 来自一个 1000 个订阅者的性能问题

1.26.0 之前,watch 的 notify_rx 就是一个普通的 Notify。2023 年 1 月的 issue #5403 描述了一个典型用法:大量订阅者,每个订阅者一个小任务,循环 changed().await、读最新值、处理、发出去、再等。复现程序是 1000 个接收任务,发送端每睡 1 毫秒发一次。从 current_thread 换到多线程运行时后,实际收到的消息数大幅下降;原本以为各订阅者并行处理会把 CPU 跑满,结果 8 个 vCPU 的机器上 CPU 一直只有 130%~180%。2023 年 2 月的 #5464(1.26.0)引用这个 issue,加了 BigNotify,把接收端分到 8 个 Notify 上。

实验:中间值被合并

use std::time::Duration;
use tokio::sync::watch;

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
  let (tx, mut rx) = watch::channel(0);
  for i in 1..=3 {
    tx.send(i).unwrap();
  }
  rx.changed().await.unwrap();
  println!("after 3 sends: changed() ok, value = {}", *rx.borrow_and_update());
  let r = tokio::time::timeout(Duration::from_millis(100), rx.changed()).await;
  println!("second changed(): {}", if r.is_ok() { "ready" } else { "timed out" });
  drop(tx);
  println!("after drop(tx): changed() = {:?}", rx.changed().await);
}

(start_paused 需要 test-util feature,让 timeout 不必真等 100 毫秒。)

after 3 sends: changed() ok, value = 3
second changed(): timed out
after drop(tx): changed() = Err(RecvError(()))

三次 send 只让 changed() 完成一次,读到的是 3,1 和 2 看不到。watch 只保存一个值,接收端只比较版本号是否不同,不数差了几个版本。

broadcast:环形缓冲区

Shared:

struct Shared<T> {
    /// slots in the channel.
    buffer: Box<[Mutex<Slot<T>>]>,

    /// Mask a position -> index.
    mask: usize,

    /// Tail of the queue. Includes the rx wait list.
    tail: Mutex<Tail>,
    // ...
}

struct Tail {
    /// Next position to write to.
    pos: u64,

    /// Number of active receivers.
    rx_cnt: usize,

    /// True if the channel is closed.
    closed: bool,

    /// Receivers waiting for a value.
    waiters: LinkedList<Waiter>,
}

struct Slot<T> {
    /// Remaining number of receivers that are expected to see this value.
    // ...
    rem: usize,

    /// Uniquely identifies the `send` stored in the slot.
    pos: u64,

    /// The value being broadcast.
    // ...
    val: Option<T>,
}
  • 容量在创建时向上取到 2 的幂(new_with_receiver_count 里的 next_power_of_two),这样位置到下标只要 pos & mask。channel(3) 实际有 4 个槽。
  • 每个槽是一把 Mutex。1.54.1 里不是 RwLock:2025 年的一个修复(4b174ce2)把它从 RwLock 换成了 Mutex,提交说明的理由是:broadcast 不要求 T: Sync,多个接收者在读锁下同时 clone() 同一个值是不健全的。send 里那句提到 writer lock 的注释是换锁之前留下的。
  • 每个 Receiver 只记一个 next: u64,即下一个要读的位置。

发送(Sender::send):锁 tail;没有接收者就返回错误;取 tail.pos 作为位置,tail.pos 加一;锁对应的槽,写入 pos、rem = rx_cnt、val。槽里原来的值被直接覆盖,不管还有没有接收者没读它。放槽锁后调 notify_rx,把等待链表整个搬走,按 32 个一批叫醒(和 batch_semaphore 一样用 WakeList),返回接收者数量。发送永远不等待:慢的接收者不会拖住发送端,代价由接收者自己承担。

接收(recv_ref)锁 next & mask 那个槽:

  • slot.pos == next:正是要读的,返回一个拿着槽锁的 RecvGuard,接收者在锁内 clone() 值。RecvGuard drop 时 rem 减一,减到 0 就把 val 丢掉(Drop for RecvGuard)。
  • 对不上:放槽锁,按「先 tail 后槽」的顺序重新加锁(send 是这个顺序,反过来会死锁),再判断一次:
    • slot.pos + 容量 == next:槽里是上一圈的值,这个接收者已经读到最新了。channel 关了就返回 Closed,否则把 waker 挂进 tail.waiters,返回 Empty。
    • 否则,槽已经被覆盖过不止一圈,接收者落后了。

落后时的处理:

// At this point, the receiver has lagged behind the sender by
// more than the channel capacity. The receiver will attempt to
// catch up by skipping dropped messages and setting the
// internal cursor to the **oldest** message stored by the
// channel.
let next = tail.pos.wrapping_sub(self.shared.buffer.len() as u64);

let missed = next.wrapping_sub(self.next);

drop(tail);

// The receiver is slow but no values have been missed
if missed == 0 {
    self.next = self.next.wrapping_add(1);

    return Ok(RecvGuard { slot });
}

self.next = next;

return Err(TryRecvError::Lagged(missed));

next 跳到缓冲区里最老的那条(tail.pos - 容量),missed 就是跳过的条数,作为 Lagged(missed) 返回一次。接收者没有被关闭,下一次 recv 从最老的那条接着读。

实验:Lagged(6)

use tokio::sync::broadcast;

#[tokio::main(flavor = "current_thread")]
async fn main() {
  // capacity 3 is rounded up to 4
  let (tx, mut rx) = broadcast::channel::<u32>(3);
  for i in 0..10 {
    tx.send(i).unwrap();
  }
  println!("rx.len() = {}", rx.len());
  loop {
    match rx.try_recv() {
      Ok(v) => println!("recv {v}"),
      Err(e) => {
        println!("err {e:?}");
        if matches!(e, broadcast::error::TryRecvError::Empty) {
          break;
        }
      }
    }
  }
}
rx.len() = 10
err Lagged(6)
recv 6
recv 7
recv 8
recv 9
err Empty

channel(3) 有 4 个槽。发了 10 条,tail.pos 是 10,最老的是位置 6,所以 missed = 6 - 0 = 6,之后读到 6、7、8、9。如果容量没有取整,跳过的就是 7 条。len() 返回 10,它算的是 tail.pos - next,包括已经被覆盖的消息。

recv 外面套了 cooperative,扣预算。

❗IMPORTANT
动脑筋:两个接收者各读到什么?
let (tx, mut rx1) = broadcast::channel::<u32>(5);
for i in 0..12 {
  tx.send(i).unwrap();
}
let mut rx2 = tx.subscribe();
for i in 12..20 {
  tx.send(i).unwrap();
}
println!("rx1: {:?} then {:?}", rx1.try_recv(), rx1.try_recv());
println!("rx2: {:?} then {:?}", rx2.try_recv(), rx2.try_recv());

提示:channel(5) 实际有几个槽?subscribe() 出来的接收者从哪个位置开始读?答案在本页末尾。

Notify:一个状态字,两种通知

Notify:

pub struct Notify {
    // `state` uses 2 bits to store one of `EMPTY`,
    // `WAITING` or `NOTIFIED`. The rest of the bits
    // are used to store the number of times `notify_waiters`
    // was called.
    //
    // Throughout the code there are two assumptions:
    // - state can be transitioned *from* `WAITING` only if
    //   `waiters` lock is held
    // - number of times `notify_waiters` was called can
    //   be modified only if `waiters` lock is held
    state: AtomicUsize,
    waiters: Mutex<LinkedList<Waiter>>,
}

状态字的布局(第 437 行):

位含义
低 2 位EMPTY(0)、WAITING(1,链表里有人)、NOTIFIED(2,存着一个许可)
其余位notify_waiters 被调用的次数

链表节点同样嵌在 Notified future 里。

💡TIP
打个比方:门口的留言夹和楼里的广播

notify_one 像往门口的留言夹上夹便条。屋里有人在等(WAITING,链表非空),就直接叫醒等得最久的那个(pop_back);没人在等,就把便条夹上(NOTIFIED),下一个来的人取走它。夹子只夹得住一张,再夹也还是一张。

notify_waiters 像楼里的广播:只有当时已经在楼里的人听得见,广播不录音,后来的人什么也听不到。楼里挂着一块计数牌,记着广播过几次(状态字的高位)。一个人进楼时(创建 Notified)先看一眼牌子记下数字,之后发现数字变了,就知道有一次广播是说给他的,哪怕广播的时候他在打盹(还没被 poll)。

比方到这里为止:便条和广播都有内容,Notify 的通知什么都不带,连发生过几次都不记。醒来以后要做什么,得自己去别处查。

notify_one:没人等就存一个许可

notify_with_strategy(notify_one 和 notify_last 都走这里):

// Load the current state
let mut curr = self.state.load(Acquire);

// If the state is `EMPTY`, transition to `NOTIFIED` and return.
while let EMPTY | NOTIFIED = get_state(curr) {
    // The compare-exchange from `NOTIFIED` -> `NOTIFIED` is intended. A
    // happens-before synchronization must happen between this atomic
    // operation and a task calling `notified().await`.
    let new = set_state(curr, NOTIFIED);
    let res = self.state.compare_exchange(curr, new, AcqRel, Acquire);

    match res {
        // No waiters, no further work to do
        Ok(_) => return,
        Err(actual) => {
            curr = actual;
        }
    }
}

// There are waiters, the lock must be acquired to notify.
let mut waiters = self.waiters.lock();

没人等时,状态变成 NOTIFIED,等于存下一个许可,下一个 notified().await 直接拿走它。已经是 NOTIFIED 再 notify_one,还是 NOTIFIED:许可最多一个,不累加。

有人等时拿锁,notify_locked 从链表里摘一个:notify_one 用 pop_back,摘最早挂上去的;notify_last 用 pop_front,摘最晚的。给它的节点标上 Notification::One,链表空了就把状态改回 EMPTY,放锁后 wake。

notify_waiters:只给已经存在的 Notified

inner_notify_waiters:

if matches!(get_state(curr), EMPTY | NOTIFIED) {
    // There are no waiting tasks. All we need to do is increment the
    // number of times this method was called.
    atomic_inc_num_notify_waiters_calls(&self.state);
    return;
}

// Increment the number of times this method was called
// and transition to empty.
let new_state = set_state(inc_num_notify_waiters_calls(curr), EMPTY);
self.state.store(new_state, Release);

不管有没有人等,调用次数都加一;有人等时,把链表整个搬到一个带哨兵节点的临时链表里,按 32 个一批标记 Notification::All 并叫醒,批与批之间放锁。它从不把状态设成 NOTIFIED,不存许可。

调用次数是给 Notified 用的。notified() 在创建 future 时就读一次状态,把调用次数存进 future:

pub fn notified(&self) -> Notified<'_> {
    // we load the number of times notify_waiters
    // was called and store that in the future.
    let state = self.state.load(Acquire);
    Notified {
        notify: self,
        state: State::Init,
        notify_waiters_calls: get_num_notify_waiters_calls(state),
        waiter: Waiter::new(),
    }
}

第一次 poll 时(poll_notified 的 Init 分支),先比较调用次数,变了就直接完成;没变再试着把 NOTIFIED CAS 成 EMPTY 拿许可;都不行才拿锁、把状态设成 WAITING、把节点挂进链表。所以一个 Notified 哪怕从没被 poll 过,只要在 notify_waiters 之前创建,就能收到这次通知。

📝NOTE
冷知识:这个计数是 0.3.4 才加的,2026 年又调过一次先后顺序

notify_waiters 原本是 crate 内部的方法。2020 年 11 月的 #3098(0.3.4)把它公开,同时加了这个调用次数。PR 说明写的理由是:之前一个 Notified 必须先被 poll 过,才算登记了对 notify_waiters 的兴趣,而 poll 之前得先把它 pin 住,用起来很别扭;有了计数,future 创建时记下次数,第一次 poll 时一比就知道,不需要提前登记。

2026 年的 issue #7965 报告了一个边角:notified1 创建后从没被 poll,接着依次调 notify_waiters()、notify_one()。提 issue 的人预期 notified1 被 notify_waiters 叫醒,notify_one 存下的许可留给之后的 notified2;实际上 notified1 第一次 poll 时先去抢 NOTIFIED 许可,把它吃掉了,notified2 拿不到。#7996(1.51.0)把 Init 分支的顺序换成现在这样:先比较调用次数,变了就直接完成,许可留给别人。

enable:先排上队

Notified::enable 就是不带 waker 地跑一次 poll_notified:能拿到许可就拿,拿不到就把节点挂进链表。它解决的是 notify_one 的问题:没被 poll 过的 Notified 不在链表里,notify_one 看不到它。这时如果链表里有别人,许可给了别人;链表是空的,许可存在状态里,谁先 poll 谁拿走。enable 之后它就在链表里,按排队顺序等 notify_one。

drop 一个收到通知的 Notified

drop_notified:如果这个 future 已经被 notify_one 选中(Notification::One),但还没被 poll 到就被 drop 了(比如在 select! 里输了),它会再调一次 notify_locked,把这次通知转给链表里的下一个;没有下一个,就存成许可。notify_one 的通知不会因为被选中的 future 取消而消失。

实验:Notify 会丢通知吗

use std::time::Duration;
use tokio::sync::Notify;
use tokio::time::timeout;

async fn wait(n: &Notify) -> &'static str {
  match timeout(Duration::from_millis(100), n.notified()).await {
    Ok(()) => "woken",
    Err(_) => "timed out",
  }
}

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
  let n = Notify::new();
  n.notify_one();
  println!("1. notify_one, then notified().await: {}", wait(&n).await);

  let n = Notify::new();
  n.notify_one();
  n.notify_one();
  println!("2. notify_one x2, first wait: {}", wait(&n).await);
  println!("   second wait: {}", wait(&n).await);

  let n = Notify::new();
  n.notify_waiters();
  println!("3. notify_waiters, then notified().await: {}", wait(&n).await);

  let n = Notify::new();
  let f = n.notified(); // created, never polled
  n.notify_waiters();
  let r = timeout(Duration::from_millis(100), f).await;
  println!("4. notified() created, notify_waiters, then await: {}", if r.is_ok() { "woken" } else { "timed out" });

  // 5. two un-enabled futures, notify_one: the permit goes to whoever polls first
  let n = Notify::new();
  let a = n.notified();
  let mut b = std::pin::pin!(n.notified());
  b.as_mut().enable();
  n.notify_one();
  let ra = timeout(Duration::from_millis(100), a).await.is_ok();
  let rb = timeout(Duration::from_millis(100), b).await.is_ok();
  println!("5. a not enabled, b enabled, notify_one: a woken={ra}, b woken={rb}");
}
1. notify_one, then notified().await: woken
2. notify_one x2, first wait: woken
   second wait: timed out
3. notify_waiters, then notified().await: timed out
4. notified() created, notify_waiters, then await: woken
5. a not enabled, b enabled, notify_one: a woken=false, b woken=true

第 5 组里 a 先被 poll,但 b 已经 enable 进了链表,notify_one 直接把通知给了 b,没有存许可,a 什么也拿不到。

所以「Notify 会丢通知吗」的准确回答:

  • notify_one 不会丢,但会合并。 没人等时存成一个许可;被选中的 future 在完成前被 drop,通知转给下一个等待者或存成许可。但许可只有一个,没人等时连续 notify_one 多次,只有一个 notified() 能完成(第 2 组)。用 Notify 数事件个数是错的,它表达的是「至少发生过一次」。
  • notify_waiters 对之后创建的 future 无效。 它不存许可,只叫醒调用之前已经 notified() 出来的 future,不论有没有 poll 过(第 3、4 组)。
  • 正确的等待写法是先 notified()(需要 notify_one 时还要 enable()),再检查条件,最后 await。watch 的 changed_impl、mpsc 的 Sender::closed() 都是这么写的。先检查条件、再创建 notified(),两步之间的 notify_waiters 就会错过。

Notify 的 future 不调 poll_proceed,不扣协作式预算。

怎么选

原语发送 / 接收存多少发送会等吗慢的接收者典型用途
oneshot1 对 11 个值不等—请求-响应:把结果送回发起者
mpsc::channel(n)多对 1n 条满了等,FIFO拖住发送端(背压)任务队列、actor 的信箱
mpsc::unbounded_channel多对 1不限不等内存增长不能 .await 的地方发消息(比如同步代码、Drop)
broadcast::channel(n)多对多n 条,取整到 2 的幂不等,覆盖最老的收到 Lagged(k),跳过 k 条事件广播,每个订阅者都要看到每条(尽量)
watch1 对多1 个值不等只看到最新值配置、状态、关闭信号
Notify不带数据最多 1 个许可不等—自己写同步原语时的「叫醒」部分

几个容易选错的地方:

  • 只关心「最新状态」时用 watch,不要用 broadcast:broadcast 会把每个中间值都送一遍,还可能 Lagged。
  • broadcast 的值在每个接收者处 clone() 一次,大对象应该包在 Arc 里发。
  • Notify 只说「有事发生」,事件的内容和个数要放在别处(一个原子变量、一把锁里的队列),醒来后自己去查。

动脑筋答案

放进 #[tokio::main(flavor = "current_thread")] 里跑(M2 Max、rustc 1.99.0、--release),连跑 3 次,输出都一样:

rx1: Err(Lagged(12)) then Ok(12)
rx2: Ok(12) then Ok(13)
  • channel(5) 向上取到 2 的幂,有 8 个槽。一共发了 20 条,tail.pos 是 20,缓冲区里留着的是位置 12 到 19。
  • rx1 从位置 0 开始读,槽里早已被覆盖。它跳到最老的那条,missed = 12 - 0 = 12,返回 Lagged(12);下一次读到 12。如果容量没有取整,最老的是位置 15,就会是 Lagged(15)。
  • rx2 是发完 12 条以后 subscribe() 的,next 从当时的 tail.pos,也就是 12 开始。之后正好发了 8 条,填满 8 个槽,位置 12 还没被覆盖,所以它一条没丢,从 12 读起。再多发 1 条,rx2 也会收到 Lagged(1)。

Lagged 是按接收者算的:同一个 channel,订阅得早、读得慢的那个落后,订阅得晚的不受影响。

小结

  • oneshot:一个 Arc,一个 4 位状态字,两个内联的 waker 槽。VALUE_SENT 决定 value 归谁;发送端 drop 也走 complete,接收端看到空值即报错。
  • watch:同步 RwLock 保存值,版本号每次加 2(第 0 位是关闭标志),接收端只比版本是否不同,中间值被合并。接收端分散在 8 个 Notify 上以减少锁争用。
  • broadcast:容量取整到 2 的幂的环形缓冲区,每槽一把 Mutex。发送不等待,直接覆盖最老的槽;接收者落后超过一圈时跳到最老的消息,返回一次 Lagged(跳过的条数)。
  • Notify:状态字低 2 位是 EMPTY / WAITING / NOTIFIED,其余位数 notify_waiters 的调用次数。notify_one 没人等就存一个许可(不累加);notify_waiters 不存许可,只对创建时间早于它的 Notified 生效,判断依据是创建时记下的调用次数。
  • 等待条件的正确顺序:先 notified()(必要时 enable()),再检查条件,再 await。

💬 关联讨论

发起讨论
加载中...