ZeroAllocJobScheduler

repository·master·Indexed 42 days ago

https://github.com/genaray/zeroallocjobscheduler

A high-performance, allocation-free C# job scheduler designed to execute tasks on worker threads with minimal garbage collection overhead. It supports basic IJob tasks, sequential job dependencies, and IJobParallelFor for large iterations.

Tokens
1.6K
Snippets
6
Records
6
Agent score
3%

What's inside ZeroAllocJobScheduler

  1. Use IJobParallelFor for large iterations

    master

    For high-volume, small operations (like iterating over a large dataset), implement IJobParallelFor instead of IJob. This is more efficient than scheduling thousands of individual jobs.

    Implementation Requirements:

    • Execute(int i): The logic to run for each index i.
    • Finish(): Logic to run once all indices are processed.
    • BatchSize (int): Determines how many indices are processed per thread per dispatch.
      • High BatchSize: Minimizes scheduler overhead for very small tasks.
      • Low BatchSize: Maximizes work-stealing efficiency for complex/mixed tasks.
    • ThreadCount (int): Number of spawned jobs. Set to 0 to use the scheduler's active thread count.

    Usage: Pass the job and the total number of iterations to scheduler.Schedule(job, count).

    public class ManyCalculations : IJobParallelFor
    {
      public void Execute(int i) { /* ... */ }
      public void Finish() { /* ... */ }
      public int BatchSize => 32;
      public int ThreadCount => 0;
    }
    
    // ...
    var job = new ManyCalculations();
    var handle = scheduler.Schedule(job, 512); // Executes 512 times
  2. Schedule and execute basic IJob tasks

    master

    To run a task, implement the IJob interface and its Execute() method. Because the scheduler is zero-allocation, you are responsible for pooling and reusing your job instances to avoid garbage collection.

    Workflow:

    1. Create/Reuse an IJob instance.
    2. Call scheduler.Schedule(job) to get a JobHandle.
    3. Call scheduler.Flush() to dispatch all locally scheduled jobs to worker threads.
    4. Call handle.Complete() to block the current thread until that specific job is finished.
    5. Call scheduler.Dispose() at program exit to shut down worker threads.
    public class HeavyCalculation : IJob
    {
      public void Execute()
      {
        Thread.Sleep(50); 
        Console.WriteLine("Done");
      }
    }
    
    // ... setup scheduler ...
    
    var firstJob = new HeavyCalculation();  
    var firstHandle = scheduler.Schedule(firstJob);
    
    scheduler.Flush();                              
    firstHandle.Complete();                         
    
    scheduler.Dispose();                
  3. Initialize the JobScheduler

    master

    To use the scheduler, instantiate a JobScheduler with a JobScheduler.Config object. The JobScheduler instance should be kept alive for the duration of your program. Note that this is the only API call that will allocate or generate garbage.

    Configuration Options:

    • ThreadPrefixName (string): Names the worker threads (e.g., "MyProgram0", "MyProgram1").
    • ThreadCount (int): Number of worker threads. Set to 0 to automatically choose based on processor count.
    • MaxExpectedConcurrentJobs (int): The capacity of the internal queue. To maintain zero-allocation behavior, ensure your concurrent job count stays below this number. Higher values increase memory consumption and slightly decrease performance.
    • StrictAllocationMode (bool): If true, the scheduler throws an error if the number of scheduled jobs exceeds MaxExpectedConcurrentJobs. Useful for debugging, but not recommended for production.
    var scheduler = new JobScheduler(new JobScheduler.Config()
    {
        ThreadPrefixName = "MyProgram",
        ThreadCount = 0,
        MaxExpectedConcurrentJobs = 64,
        StrictAllocationMode = false,
    });
  4. Combine multiple dependencies into one handle

    master

    If you have multiple jobs running in parallel and need a subsequent job to wait for all of them to finish, use Scheduler.CombineDependencies(JobHandle[] handles). This returns a single JobHandle that represents the completion of the entire group.

    JobHandle[] handles = new JobHandle[2];
    
    handles[0] = scheduler.Schedule(job1);
    handles[1] = scheduler.Schedule(job2);
    
    // Creates a single handle that depends on both job1 and job2
    JobHandle combinedHandle = Scheduler.CombineDependencies(handles);
    
    // job3 will only run once both job1 and job2 are complete
    var dependantHandle = scheduler.Schedule(job3, combinedHandle);
    
    dependantHandle.Complete();
  5. Set sequential job dependencies

    master

    You can ensure one job only starts after another has finished by passing the JobHandle of the first job as a dependency to the second job's Schedule call.

    var handle1 = scheduler.Schedule(job1);
    var handle2 = scheduler.Schedule(job2, handle1); // job2 waits for job1
    scheduler.Flush();
  6. Wait for multiple handles to complete

    master

    If you simply need to block the main thread until a collection of jobs is finished (without creating a new dependency handle), use the static JobHandle.CompleteAll method. This supports both arrays and lists of JobHandle objects.

    // Blocks until all handles in the array are finished
    JobHandle.CompleteAll(JobHandle[] handles);
    
    // Blocks until all handles in the list are finished
    JobHandle.CompleteAll(List<JobHandle> handles);