DOTS//CORE local · not synced

m04 · Jobs · reading · 10 min

Dependencies — the graph that keeps parallelism correct

Not everything is independent, and that’s the point of handles

Chunks within one job are independent. But jobs often aren’t: a job that computes forces must finish before the job that integrates them from those forces; a job that builds a spatial grid must finish before the job that queries it. The safety system already knows these jobs conflict (one writes what the other reads). What it needs from you is the ordering — which finishes first — and the vehicle for that is the JobHandle.

Every Schedule/ScheduleParallel call returns a JobHandle, a token representing “this scheduled work.” You pass a handle into another schedule call to say “this new job depends on that one — don’t start it until that handle’s job is done”:

JobHandle forces = new ComputeForcesJob { ... }.ScheduleParallel(default);
JobHandle move   = new IntegrateJob   { ... }.ScheduleParallel(forces);
// 'move' will not begin until 'forces' has finished

Chain enough of these and you’ve described a dependency graph: a set of jobs with edges saying which must precede which. The job system reads that graph and schedules accordingly — running independent branches concurrently across cores, and serializing only where you declared a dependency. You never wrote a lock or a signal. You expressed ordering as data (handles passed as arguments), exactly as Module 1 said you would have to when iteration order matters: not assumed, but encoded.

Scheduling is deferred; the handle is a promise

Here is the timing model, and it’s easy to get backwards. Schedule does not run the job. It returns almost immediately, handing you a handle, while the actual work happens later on worker threads — possibly not starting until the system decides to flush pending jobs. The handle is a promise about work that is queued, not a report about work that is done.

This deferral is what enables the parallelism. Because scheduling is cheap and non-blocking, you can schedule a whole graph of jobs in a few main-thread microseconds, and the system then runs that entire graph across all cores while the main thread moves on. If Schedule blocked until the work finished, there would be no parallelism — you’d have an elaborate way to run jobs one at a time. The point of a handle is that you can keep scheduling — build the graph — without waiting for any of it.

The complement is Complete. Calling handle.Complete() forces the main thread to wait until that job (and everything it depends on) has finished, and only then is it safe to read the results on the main thread. Complete is the moment the promise is redeemed. You call it when you genuinely need the output now on the main thread — and, crucially, not before.

cache line · 64 B fetched 64 B useful · 100% of the bandwidth you paid for a dependency graph: independent branches run on separate cores at once, edges (handles) force ordering only where declared — the shape you schedule, the runtime executes

Over-completing is how you delete your own parallelism

The most common performance mistake with jobs is not a race — the safety system catches those. It’s calling Complete too early or too often, turning a parallel graph back into serial main-thread work. Each Complete is a main-thread stall: the main thread stops and waits. Do it after every job — schedule, complete, schedule, complete — and you have destroyed the entire benefit. The jobs run one at a time, each followed by the main thread blocking on it, which is strictly worse than not using jobs at all (you paid scheduling overhead for nothing).

This is the same shape as the sync point from Module 2, and now you can see they’re the same phenomenon. A structural-change sync point drains outstanding jobs before it can safely move memory — it’s a forced Complete of everything in flight. An over-eager Complete is a self-inflicted version of that same barrier. Both stall the main thread waiting for jobs; both throw away concurrency; both are minimized by the same discipline — let work stay in flight as long as possible, and force completion as late and as rarely as you can. Schedule broadly, complete narrowly.

Systems make this mostly automatic. A system’s state.Dependency carries the handle of the work it scheduled; the framework threads that dependency into the next system and completes at appropriate boundaries. Feeding your handles through state.Dependency — taking it as your input dependency, storing your output back into it — lets the framework manage completion timing, which is usually better than any hand-placed Complete.

What this buys you

You can now express ordering between jobs with handles, you understand that scheduling defers while Complete forces a wait, and you can see that over-completing is the same main-thread-stall as a Module 2 sync point — to be pushed as late and made as rare as possible. The final lesson gathers the recurring job patterns — parallel-for over an array, the classic pitfalls of writing to shared output — and closes the module by tying the whole parallel story back to the frame. Then the lab makes you measure a real parallel speedup and catch yourself over-completing.

m04.l05