How to spread Jobs on multiple frames?

Summary

The job system completed all work in a single frame because no asynchronous boundary existed between scheduling and completion. Unity’s Job System will happily execute all pending jobs immediately in the same frame if the main thread becomes idle or if the job dependencies allow it. The result is that work you expected to be spread across frames instead executes as a burst of synchronous computation.

Root Cause

The core issue is that scheduling a job does not guarantee multi‑frame execution. Unity will run jobs as soon as possible unless something explicitly prevents it. In this case:

  • The main thread never yields long enough for jobs to spill into the next frame.
  • No artificial frame boundary (e.g., JobHandle stored and checked later) prevents Unity from executing the job immediately.
  • Batch size = 1 forces extremely fine-grained work stealing, encouraging the worker threads to finish everything quickly.
  • No dependency chain forces the job to wait for the next frame.
  • JobHandle.ScheduleBatchedJobs() flushes the queue, encouraging immediate execution.

Why This Happens in Real Systems

Real-world job systems are designed to maximize throughput, not to enforce multi-frame scheduling. Unity’s worker threads aggressively consume all available work:

  • Worker threads are always hungry — if work is available, they execute it immediately.
  • The main thread is not blocked, so Unity sees no reason to defer work.
  • Jobs are not time-sliced; they run until finished unless you explicitly break them into smaller jobs.
  • Scheduling does not imply asynchronous execution; it only queues work.

Real-World Impact

This behavior leads to several common problems:

  • Unexpected frame spikes when heavy jobs finish in the same frame.
  • No opportunity to interleave work across frames.
  • Chained jobs execute back-to-back, causing long stalls.
  • Developers mistakenly assume jobs behave like coroutines, but they do not.

Example or Code (if necessary and relevant)

A multi-frame job requires manual chunking. For example, instead of scheduling one job for all chunks, schedule a job per chunk group:

public struct ChunkStepJob : IJob
{
    public int start;
    public int count;
    public NativeArray results;

    public void Execute()
    {
        for (int i = start; i < start + count; i++)
            results[i] = Mathf.Sqrt(i);
    }
}

Then schedule only a portion per frame:

if (currentBatch < totalBatches)
{
    var job = new ChunkStepJob
    {
        start = currentBatch * batchSize,
        count = batchSize,
        results = results
    };

    handle = job.Schedule();
    currentBatch++;
}

This forces multi-frame execution because you only schedule a small portion each frame.

How Senior Engineers Fix It

Experienced engineers solve this by explicitly designing multi-frame pipelines:

  • Chunk the workload into small, independent batches.
  • Schedule only one batch per frame, not the entire workload.
  • Use persistent JobHandles to track progress.
  • Avoid ScheduleBatchedJobs() unless necessary.
  • Increase batch size to reduce overhead but still maintain multi-frame behavior.
  • Use IJobParallelFor with large batch sizes to avoid immediate full completion.
  • Design job graphs where each frame schedules the next stage.

The key is that multi-frame behavior must be engineered, not assumed.

Why Juniors Miss It

Less experienced developers often assume:

  • Jobs behave like coroutines — they do not.
  • Scheduling implies asynchronous execution — it does not.
  • Unity automatically spreads work across frames — it never does unless forced.
  • Batch size = 1 means slower execution — in reality, it increases parallelism.
  • IsCompleted() means “wait until next frame” — but the job may have already finished instantly.

They expect the job system to enforce pacing, but Unity enforces throughput, not pacing.


Would you like a rewritten version of your original code that correctly spreads the workload across frames?

Leave a Comment