Skip to content

一写多读

第二天,老板搞起促销:撞右边的金护栏算 3 分,普通护栏 1 分——除了响动,还得有人计分。

按共享数据的老路,这意味着改造 drive:让它认识记分牌资源、学会算分、还得判断撞的是哪边。消息给出的答案完全不同:写者只在消息里多放一点数据,计分是新读者自己的事。

消息先升级,带上“撞的是哪边”:

rust
/// 直道两端的护栏:促销期间,撞右边的金护栏算 3 分
#[derive(Clone, Copy)]
enum Rail {
    Left,
    Right,
}

/// 消息升级:除了"撞了",还带上撞的是哪边
#[derive(Message)]
struct RailHit {
    rail: Rail,
}

三个系统各司其职——车手报告事实,DJ 和记分员各取所需:

rust
/// 写者只管报告事实,不关心谁在听
fn drive(mut car: Single<&mut Car>, mut hits: MessageWriter<RailHit>) {
    car.pos += car.velocity;
    if car.pos == 0 || car.pos == 4 {
        car.velocity = -car.velocity;
        let rail = if car.pos == 0 { Rail::Left } else { Rail::Right };
        hits.write(RailHit { rail });
    }
}

/// 读者一:DJ 放音效——不关心撞的是哪边
fn play_sound(mut hits: MessageReader<RailHit>) {
    for _ in hits.read() {
        println!("DJ:砰!");
    }
}

/// 读者二:记分员——按消息携带的数据区别对待
fn update_score(mut hits: MessageReader<RailHit>, mut score: ResMut<Score>) {
    for hit in hits.read() {
        let (name, points) = match hit.rail {
            Rail::Left => ("普通护栏", 1),
            Rail::Right => ("金护栏", 3),
        };
        score.0 += points;
        println!("记分员:{name} +{points},阿莱共 {} 分", score.0);
    }
}

接线时只有一处新东西:两个读者打包在元组里,彼此不分先后:

rust
fn main() {
    let mut app = App::new();
    app.add_message::<RailHit>()
        .init_resource::<Score>()
        .add_systems(Startup, spawn_car)
        // 写者在前;两个读者之间不分先后,可以并行
        .add_systems(Update, (drive, (play_sound, update_score)).chain());

    for frame in 1..=4 {
        println!("—— 第 {frame} 帧 ——");
        app.update();
    }
}

Listing 7-3:一写多读——DJ 与记分员各自读到全部碰撞

console
cargo run -p ch07-messages --example listing-07-03
text
—— 第 1 帧 ——
记分员:金护栏 +3,阿莱共 3 分
DJ:砰!
—— 第 2 帧 ——
记分员:普通护栏 +1,阿莱共 4 分
DJ:砰!
—— 第 3 帧 ——
DJ:砰!
记分员:金护栏 +3,阿莱共 7 分
—— 第 4 帧 ——
记分员:普通护栏 +1,阿莱共 8 分
DJ:砰!

还是那条 4 格直道,左右护栏轮流挨撞,分数 3、4、7、8 涨得分毫不差。对账:

  • 两个读者都收到了每一次碰撞,各恰好一次。DJ 响了四声,记分员记了四笔,谁也没漏、谁也没重。上一节说过 MessageReader 内部是 Res 加一个 Local 游标——Local 是每个系统私有的(第 4 章),所以每个读者各自记录“我读到哪了”,互不干扰;消息本体留在缓冲里没动,read() 给出的是引用。
  • 数据驱动差异。DJ 不关心护栏的身份,连模式匹配都省了;记分员按 rail 字段区别对待。同一条消息,读者各取所需。
  • DJ 和记分员两行的先后在漂——第 3 帧 DJ 在前,其余三帧记分员在前。这不是 bug:两个读者之间没有任何排序约束,而读取只需要共享访问(Res + 各自的私有游标),调度器尽可以让它们并行运行,打印交错纯属正常。你跑出来的顺序多半和书里不同,多跑几次自己还会变。第 6 章的判断标准在这里复述一遍:它们没有冲突的数据访问,无序是无歧义的。

现在掂一掂解耦的分量。老板明天要“连击播报员”、后天要成就系统,答案全是一样的:加一个读者,写者和既有读者一行不动。反过来同样成立——多个系统可以各持一个 MessageWriter<RailHit> 往同一条通道写(多写一读、多写多读都行),消息按写入顺序排队,所有读者按同一顺序读到。唯一的代价是写者之间不能并行:MessageWriter 本质是 ResMut,独占规则照旧。

到目前为止,我们一直小心地让写者排在读者前面。要是哪天接反了呢?下一节故意把链子倒过来——答案会把我们引向消息缓冲的内部构造。