m04 · Jobs · reading · 10 min
Dependencies — the graph that keeps parallelism correct
Not everything is independent, and that’s the point of handles
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.
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.