feat(JobScheduler): improve dependency handling logic

Updated `JobScheduler` to enhance dependency tracking by counting valid dependencies upfront and dynamically adjusting counts using `Interlocked` operations. Improved job enqueueing logic to ensure jobs are only enqueued when all dependencies are met.

Replaced `Interlocked.Increment` with `Interlocked.Add` for batch updates to `_totalJobCount`, improving performance. Adjusted `VirtualStack` cleanup to use the correct size variable for memory deallocation.

Simplified `JobDispatchingJob` API by removing `ctx.ThreadIndex` parameter. Updated `TestJobSystem` to pass job handles as dependencies for proper execution order.

Incremented assembly version to 1.5.9 to reflect these changes.
This commit is contained in:
2026-04-12 22:09:28 +09:00
parent 9c4faa107a
commit 8b7f773d29
6 changed files with 33 additions and 35 deletions

View File

@@ -228,8 +228,6 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable
private bool _disposed = false;
internal volatile int _totalJobCount;
internal bool IsCancellationRequested => _cts.IsCancellationRequested;
public int WorkerCount => _workerThreads.Length;
@@ -242,11 +240,11 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable
{
var workerCount = Math.Max(1, threadCount);
_jobInfoPool = new();
_jobQueue = new();
_jobInfoPool = new ConcurrentSlotMap<JobInfo>();
_jobQueue = new ConcurrentQueue<JobHandle>();
_workSignal = new(0);
_cts = new();
_workSignal = new SemaphoreSlim(0);
_cts = new CancellationTokenSource();
_workerThreads = new WorkerThread[workerCount];
@@ -297,13 +295,24 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable
jobQueue.Enqueue(handle);
}
Interlocked.Increment(ref _totalJobCount);
_workSignal.Release(handleCount);
}
}
private JobHandle CreateJobHandle(ref JobInfo jobInfo, params ReadOnlySpan<JobHandle> dependencies)
{
var validDepCount = 0;
for (var i = 0; i < dependencies.Length; i++)
{
if (dependencies[i].IsValid)
{
validDepCount++;
}
}
// Advance count to account for all dependencies upfront + 1 guard lock
jobInfo.dependencyCount = validDepCount + 1;
var id = _jobInfoPool.Add(jobInfo, out var generation);
ref var infoInPool = ref _jobInfoPool.GetElementReferenceAt(id, generation, out _);
@@ -321,13 +330,13 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable
if (!exist)
{
// Dependency does not exist (likely completed already)
Interlocked.Decrement(ref infoInPool.dependencyCount);
continue;
}
// Lock-free registration: Try to acquire "Reader Lock" by incrementing RC in high bits.
// If state is already Completed, we skip (dependency met).
var registered = false;
var completed = false;
var spin = new SpinWait();
while (true)
@@ -337,7 +346,6 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable
if (state == JobState.Completed)
{
completed = true;
break;
}
@@ -374,20 +382,18 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable
spin.SpinOnce(-1);
}
if (!registered && !completed)
// If we didn't successfully register (completed fast), drop it from the advanced counter
if (!registered)
{
// Should not happen if logic is correct, unless loop logic changed
Interlocked.Increment(ref infoInPool.dependencyCount);
Interlocked.Decrement(ref infoInPool.dependencyCount);
}
else if (registered)
{
// Successfully added dependency
Interlocked.Increment(ref infoInPool.dependencyCount);
}
// else: completed is true, registered is false -> Dependency is already done, so we don't increment our dependencyCount.
}
EnqueueJobIfReady(handle);
// Lower the initial 1 guard lock; Enqueue if met
if (Interlocked.Decrement(ref infoInPool.dependencyCount) == 0)
{
EnqueueJobIfReady(handle);
}
return handle;
}
@@ -509,7 +515,6 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable
NativeMemory.Free(info.pJobData);
_jobInfoPool.Remove(handle.ID, handle.Generation);
Interlocked.Decrement(ref _totalJobCount);
for (var i = 0; i < dependentCount; i++)
{

View File

@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<AssemblyVersion>1.5.8</AssemblyVersion>
<AssemblyVersion>1.5.9</AssemblyVersion>
<Version>$(AssemblyVersion)</Version>
<Authors>Misaki</Authors>
<PackageProjectUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</PackageProjectUrl>

View File

@@ -33,12 +33,6 @@ internal class WorkerThread : IDisposable
private bool TryFindJob(out JobHandle handle)
{
if (Interlocked.CompareExchange(ref _scheduler._totalJobCount, 0, 0) == 0)
{
handle = JobHandle.Invalid;
return false;
}
if (_localQueue.TryDequeue(out handle))
{
return true;
@@ -112,14 +106,12 @@ internal class WorkerThread : IDisposable
if (jobInfo.pExecutionFunc != null)
{
var ctx = new JobExecutionContext(_index, _scheduler);
if (!jobInfo.pExecutionFunc(jobInfo.pJobData, ref jobInfo.jobRanges, ref jobInfo.remainingBatches, in ctx))
if (jobInfo.pExecutionFunc(jobInfo.pJobData, ref jobInfo.jobRanges, ref jobInfo.remainingBatches, in ctx))
{
// If the job returns false, it means it we are not the last worker to process this job, so we should not mark it as complete yet.
continue;
// If the job returns true, it means we are the last worker to process this job.
_scheduler.MarkJobComplete(handle);
}
}
_scheduler.MarkJobComplete(handle);
}
}
}

View File

@@ -216,12 +216,13 @@ public unsafe struct VirtualStack : IMemoryAllocator<VirtualStack, VirtualStack.
}
var ptr = _baseAddress;
var size = _reserveCapacity;
_baseAddress = null;
_allocatedOffset = 0;
_committedSize = 0;
_reserveCapacity = 0;
Munmap(ptr, _reserveCapacity);
Munmap(ptr, size);
}
}

View File

@@ -28,7 +28,7 @@ internal struct JobDispatchingJob : IJobParallelFor
data = data[loopIndex]
};
var handle = ctx.JobScheduler.ScheduleParallelFor(in innerJob, data[loopIndex].Length, 64, ctx.ThreadIndex);
var handle = ctx.JobScheduler.ScheduleParallelFor(in innerJob, data[loopIndex].Length, 64);
handles.AddNoResize(handle);
}
}

View File

@@ -199,7 +199,7 @@ public unsafe class TestJobSystem
};
var handle1 = s_jobScheduler.ScheduleParallel(ref addJob, arraySize, 64);
var handle2 = s_jobScheduler.ScheduleParallel(ref multiplyJob, arraySize, 64);
var handle2 = s_jobScheduler.ScheduleParallel(ref multiplyJob, arraySize, 64, handle1);
var handle3 = s_jobScheduler.Schedule(ref sumJob, handle2);
s_jobScheduler.Wait(handle3);