DOTS//CORE local · not synced

m04 · Jobs · reading · 9 min

Jobs — parallelism without the usual catastrophe

The reason parallel code is feared

You have many cores. Your movement system walks ten thousand entities, each independent of the others. Nothing about the problem is sequential — so why is spreading it across eight threads considered one of the hardest things in systems programming? Because the traditional tools make it hard in a precise way, and naming that way tells you what the Jobs system had to fix.

Spawn threads by hand and share mutable memory between them, and you invite the data race: two threads touching the same memory at the same time, at least one writing. The result isn’t a clean crash. It’s corruption that depends on timing — a value half-written, a count off by an amount that changes every run, a bug that appears once in ten thousand frames on the customer’s machine and never on yours. Race conditions are feared not because they’re common but because they’re nondeterministic: they don’t reproduce, they don’t show in a stack trace, and the usual debugging loop (reproduce, inspect, fix) breaks down when the bug refuses to reproduce.

Every other hazard of manual threading — deadlocks from lock ordering, priority inversions, the sheer cognitive load of reasoning about all possible interleavings — compounds this. The traditional answer is discipline: locks, careful protocols, code review, hope. The DOTS answer is structural, and it starts by changing what “parallel work” even is.

A job is data plus a function, and that’s the whole trick

A job is not a thread. It’s a struct: some data fields, and an Execute method that operates on them.

[BurstCompile]
struct AddJob : IJob
{
    [ReadOnly] public NativeArray<float> A;
    [ReadOnly] public NativeArray<float> B;
    public NativeArray<float> Result;

    public void Execute()
    {
        for (int i = 0; i < Result.Length; i++)
            Result[i] = A[i] + B[i];
    }
}

Look at what this shape makes visible. The job’s entire relationship with memory is declared in its fields: it reads A and B (marked [ReadOnly]), it writes Result. There are no hidden globals it might touch, no captured references reaching into arbitrary heap objects — Burst’s subset already forbade those. A job’s data dependencies are not buried in the body where only careful reading would find them; they are the fields of the struct, right there in the type.

That is the property that removes the catastrophe. If the system can see, from the fields alone, exactly what each job reads and writes, then it can check — before running anything — whether two jobs conflict. Two jobs that only read the same array don’t conflict. A job that writes an array conflicts with any other job reading or writing that same array. This is a decidable question when the accesses are declared, and undecidable in general when threads can touch anything. The job’s shape — data as fields, work as Execute — is what turns “is this parallel code safe?” from a matter of discipline into a matter the machine can verify.

You describe work; the system owns the threads

Notice what you did not do above: create a thread, join a thread, or manage a thread pool. You wrote a description of work. The job system owns a pool of worker threads — typically one per core — and you hand it jobs to run. It decides which thread runs which job and when, keeping all cores fed without you ever naming a thread.

This inversion is the same one from every prior module. You don’t tell the system how to schedule across cores any more than you told it how to lay out entities in memory or when exactly to run systems. You declare the work and its data, and the runtime schedules it. And it can only do that — can only safely put your work on whatever thread is free — because the job’s data access is declared in its fields, so the system can guarantee it won’t hand two conflicting jobs to two threads at once.

cache line · 64 B fetched 64 B useful · 100% of the bandwidth you paid for each worker thread chews one chunk's worth of contiguous, Burst-compiled component data — many cores, each streaming its own dense span, no shared writes to race over

The three modules stack here into the full DOTS performance story. The archetype/chunk layout makes data dense and independent per chunk. Burst compiles the per-chunk loop into vectorized native code. The job system runs those compiled loops across every core at once — and the chunk is the natural unit of parallel work, because chunks are independent by construction. Dense layout, fast compilation, safe parallelism: each was built so the next could exist.

What this buys you

You now know why parallel code is feared (the nondeterministic data race), what a job is (declared data plus an Execute), and why that shape is what lets the system schedule across cores safely instead of leaving you to manual threads. The next lesson makes the safety concrete: how the job system actually detects conflicts before they run — and why that detection is the same aliasing question from the Burst module, now asked between jobs instead of within a loop.

m04.l01