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.
[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