Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Solutions: 34 — Order is the contract

Exercise 1 — Build the schedule

#![allow(unused)]
fn main() {
fn tick(world: &mut World, dt: f32) {
    next_event(&world.pos, &world.food, &mut world.pending);

    // Per-thread to_remove segments to avoid the appliers' shared write.
    let mut seg_eat:    Vec<u32> = Vec::new();
    let mut seg_repro:  Vec<CreatureRow> = Vec::new();
    let mut seg_starve: Vec<u32> = Vec::new();

    thread::scope(|s| {
        s.spawn(|| apply_eat(&world.pending, &world.food, &mut seg_eat, /* energy partition */));
        s.spawn(|| apply_reproduce(&world.pending, &world.energy, &mut seg_repro));
        s.spawn(|| apply_starve(&world.pending, &world.id, &mut seg_starve));
    });
    // All three appliers have completed before this line.

    world.to_remove.extend(seg_eat);
    world.to_remove.extend(seg_starve);
    world.to_insert.extend(seg_repro);

    cleanup(world);
    inspect(world);
}
}

The three appliers run in parallel; their write-sets are made disjoint by per-thread segments. cleanup runs after the scope returns, never before.

Exercise 2 — Test for determinism

#![allow(unused)]
fn main() {
let mut w1 = init_world(0xCAFE);
let mut w2 = init_world(0xCAFE);

for _ in 0..100 { tick(&mut w1, 0.033); }
for _ in 0..100 { tick(&mut w2, 0.033); }

assert_eq!(hash_world(&w1), hash_world(&w2));
}

If the parallel boundaries are correct, the hashes match. The merge of seg_eat, seg_starve, and seg_repro happens in the same order on both runs (the extend calls are sequential after the scope), so to_remove and to_insert end up identical between runs.

Exercise 3 — Break the contract

thread::scope(|s| {
    s.spawn(|| apply_eat(&world.pending, &world.food, &mut seg_eat, /* ... */));
    s.spawn(|| apply_reproduce(&world.pending, &world.energy, &mut seg_repro));
    s.spawn(|| apply_starve(&world.pending, &world.id, &mut seg_starve));
    s.spawn(|| cleanup(world)); // running concurrently with appliers
});

If you sidestep the borrow checker (e.g. via unsafe shared pointers), cleanup may start before the appliers have written seg_*. The two runs of the simulator produce different hashes — sometimes. Sometimes the same. The bug’s intermittency is the lesson; intermittent bugs are the worst kind to debug, and the contract exists to prevent them.

Exercise 4 — Level boundaries

For the simulator:

levelsystemsreadswrites
0food_spawnfood_spawnerfood
1motionpos, vel, energy, foodpos, energy
2next_eventpos, foodpending
3apply_eat, apply_reproduce, apply_starvependingthread-local segments
4cleanupsegmentscreatures, food
5inspecteverything(nothing)

Six levels. Within each level, parallelism. Between levels, sync. Total throughput is bounded by the slowest level’s longest system.

Exercise 5 — A minimal scheduler

#![allow(unused)]
fn main() {
use std::collections::HashMap;

struct SystemDecl {
    name: &'static str,
    reads:  Vec<&'static str>,
    writes: Vec<&'static str>,
}

fn schedule<'a>(systems: &'a [SystemDecl]) -> Vec<Vec<&'a str>> {
    let mut levels: Vec<Vec<&str>> = Vec::new();
    let mut placed: HashMap<&str, usize> = HashMap::new();

    for s in systems {
        // The earliest level we can run is one after every system that
        // wrote a table we read.
        let mut my_level = 0;
        for sr in &s.reads {
            for prior in systems.iter().filter(|p| p.writes.iter().any(|w| w == sr)) {
                if let Some(&l) = placed.get(prior.name) {
                    my_level = my_level.max(l + 1);
                }
            }
        }
        if levels.len() <= my_level {
            levels.resize(my_level + 1, Vec::new());
        }
        levels[my_level].push(s.name);
        placed.insert(s.name, my_level);
    }
    levels
}
}

Around 30 lines. Topological sort + level grouping. Real schedulers add work-stealing, priority, GPU dispatch, dynamic re-balancing — but the core contract enforcement is exactly this. If your scheduler produces the right Vec<Vec<&str>>, you have a correct parallel ECS executor.