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 行):
| 位 | 常量 | 谁设置 | 含义 |
|---|---|---|---|
0b00001 | RX_TASK_SET | 接收端 | rx_task 里有接收端的 waker |
0b00010 | VALUE_SENT | 发送端 | 发送端结束了,value 归接收端 |
0b00100 | CLOSED | 接收端 | 接收端不要这个值了 |
0b01000 | TX_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 掉。
问: 有界 mpsc 的 send 要 .await,oneshot 的 send 却是普通函数。它不怕满吗?
答: 它不可能满。存值的地方是 Inner 里那个 value: UnsafeCell<Option<T>>,channel 创建时就分配好了,容量正好是 1;而 send(mut self, t: T) 按值拿走 Sender,一个 Sender 只能发一次,第二次调用连编译都过不了。所以发送端永远有地方放值,没有什么需要等。唯一的失败是接收端已经走了(CLOSED),这时也不用等,complete 置位失败,值原样作为 Err(t) 退回。接收端什么时候来取,跟发送端无关。
watch:版本号加读写锁
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()。版本在锁内递增,注释说这样接收端才能知道自己读到的值对应哪个版本。
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 个全叫。
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:环形缓冲区
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()值。RecvGuarddrop 时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,扣预算。
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:一个状态字,两种通知
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 里。
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
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 之前创建,就能收到这次通知。
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,不扣协作式预算。
怎么选
| 原语 | 发送 / 接收 | 存多少 | 发送会等吗 | 慢的接收者 | 典型用途 |
|---|---|---|---|---|---|
oneshot | 1 对 1 | 1 个值 | 不等 | — | 请求-响应:把结果送回发起者 |
mpsc::channel(n) | 多对 1 | n 条 | 满了等,FIFO | 拖住发送端(背压) | 任务队列、actor 的信箱 |
mpsc::unbounded_channel | 多对 1 | 不限 | 不等 | 内存增长 | 不能 .await 的地方发消息(比如同步代码、Drop) |
broadcast::channel(n) | 多对多 | n 条,取整到 2 的幂 | 不等,覆盖最老的 | 收到 Lagged(k),跳过 k 条 | 事件广播,每个订阅者都要看到每条(尽量) |
watch | 1 对多 | 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。