DOTS//CORE local · not synced

m04 · Jobs · reading · 10 min

The safety system — races are aliasing, one level up

Detection before execution

The previous lesson claimed the job system can check whether two jobs conflict. Here is what that means concretely, and why it’s a categorical improvement over manual threading. When you schedule a job, you declare — through its fields and their [ReadOnly]/write status — every native container it will touch and how. The job system, before running anything, compares these declarations across all scheduled jobs. If job X writes a NativeArray that job Y also reads or writes, and you haven’t told the system that X must finish before Y, it refuses to schedule — it throws an error, at schedule time, on the main thread, with a message naming the conflict.

Sit with how different this is from the race the last lesson described. The manual-threading race is nondeterministic, appears at runtime, doesn’t reproduce, and shows no stack trace. This is deterministic, appears at schedule time, reproduces every run, and points at the exact two jobs and the exact container. The safety system converts the worst class of bug — the timing-dependent memory corruption — into an ordinary, immediate, reproducible error you fix like any other. It can do this only because the accesses are declared; that’s the whole return on the job’s rigid shape.

A race is an aliasing conflict between jobs

Now the connection that ties this module to the last one. In the Burst module, aliasing was the question within a loop: can these two pointers overlap, such that a write through one disturbs a read through the other? A data race is the same question, one level up: can these two jobs touch overlapping memory, such that a write in one disturbs an access in the other — except now the two accesses aren’t sequential steps in a loop, they’re concurrent on different cores.

The structure is identical. Within a loop, overlap between the pointers you read and write makes vectorization incorrect, so the compiler must prove disjointness to optimize. Between jobs, overlap between what one writes and another accesses makes parallel execution incorrect, so the system must prove disjointness (or an ordering dependency) to schedule them concurrently. Aliasing and data races are the same phenomenon — a write and an access to overlapping memory with no ordering between them — observed at two scales. This is why [ReadOnly] shows up in both stories: it’s the disjointness promise that resolves the overlap question at either level.

cache line · 64 B fetched 64 B useful · 100% of the bandwidth you paid for two jobs, disjoint memory: each writes its own region, so they run concurrently on two cores with nothing to race over — the between-jobs version of no-alias

[ReadOnly] is parallelism, not politeness

Given that framing, [ReadOnly] earns its importance. Recall the conflict rule: two jobs writing the same container conflict; two jobs reading it do not. So marking a container [ReadOnly] doesn’t just document intent — it tells the safety system this job only reads, which means it cannot conflict with any other reader of the same data. Ten jobs that all read the same [ReadOnly] array can run simultaneously across ten cores, because none of them writes, so no overlap-with-a-write exists.

This is why [ReadOnly] is a throughput decision. An input array not marked [ReadOnly] is assumed writable, so the system must serialize every job that uses it — one at a time, even though they were all only going to read. Mark it [ReadOnly] and those same jobs parallelize. The attribute is the difference between a shared input being a bottleneck and being a free-for-all-readers resource, and it costs one word.

The promise you must not break

The safety system’s guarantee has a boundary, and it’s the same contract as Burst’s no-alias default. [ReadOnly] is a promise you make: this job does not write this container. The system trusts it and schedules readers concurrently on that basis. If you then write to a [ReadOnly] container anyway — through that field, or via a stashed pointer — you have concurrent writers the system believed were all readers, and you get the exact data race the whole apparatus exists to prevent. Silently, at runtime, timing-dependent.

The disciplined reading: mark every input [ReadOnly] that truly is one — it’s free parallelism and it’s honest — and never write through a handle you’ve declared read-only. The safety system will catch an undeclared conflict for you; it cannot catch a lie, because it has no reason to doubt your declaration.

What this buys you

You can now explain how the job system rejects races before execution, why a race is aliasing viewed between jobs rather than within a loop, and why [ReadOnly] is a parallelism multiplier whose power depends entirely on telling the truth. Next: the two job types built specifically for iterating entities — IJobEntity and IJobChunk — which take everything you know about chunks and turn per-chunk independence into per-core work.

m04.l02