379 lines
9.6 KiB
C#
379 lines
9.6 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Diagnostics;
|
|
using System.Runtime.ExceptionServices;
|
|
|
|
namespace Nerfed.Runtime;
|
|
|
|
public enum JobDeadlineMode
|
|
{
|
|
FrameCritical,
|
|
Deferred,
|
|
}
|
|
|
|
public enum JobExecutionStatus
|
|
{
|
|
Invalid,
|
|
Queued,
|
|
Running,
|
|
Completed,
|
|
Faulted,
|
|
Canceled,
|
|
}
|
|
|
|
public readonly struct JobHandle : IEquatable<JobHandle>
|
|
{
|
|
internal readonly int id;
|
|
|
|
internal JobHandle(int id)
|
|
{
|
|
this.id = id;
|
|
}
|
|
|
|
public bool IsValid => id > 0;
|
|
|
|
public bool Equals(JobHandle other)
|
|
{
|
|
return id == other.id;
|
|
}
|
|
|
|
public override bool Equals(object obj)
|
|
{
|
|
return obj is JobHandle other && Equals(other);
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return id;
|
|
}
|
|
|
|
public static bool operator ==(JobHandle left, JobHandle right)
|
|
{
|
|
return left.Equals(right);
|
|
}
|
|
|
|
public static bool operator !=(JobHandle left, JobHandle right)
|
|
{
|
|
return !left.Equals(right);
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return IsValid ? $"JobHandle({id})" : "JobHandle(Invalid)";
|
|
}
|
|
}
|
|
|
|
public sealed class JobSystem : IDisposable
|
|
{
|
|
public static readonly JobSystem Default = new JobSystem();
|
|
|
|
private Thread[] workers;
|
|
private SemaphoreSlim startSignal;
|
|
private CountdownEvent completionEvent;
|
|
|
|
private Thread[] asyncWorkers;
|
|
private SemaphoreSlim asyncSignal;
|
|
private ConcurrentQueue<int> asyncQueue;
|
|
private ConcurrentDictionary<int, AsyncJobState> asyncJobStates;
|
|
private int nextAsyncJobId;
|
|
|
|
private volatile bool running;
|
|
|
|
// Shared per-dispatch state written by main thread before workers wake.
|
|
private volatile Action<int> currentAction;
|
|
private int workCount;
|
|
private int nextIndex; // grabbed with Interlocked.Increment for work-stealing
|
|
|
|
private sealed class AsyncJobState : IDisposable
|
|
{
|
|
public readonly Action Action;
|
|
public readonly JobDeadlineMode Deadline;
|
|
public readonly string Name;
|
|
public readonly ManualResetEventSlim Completion = new(false);
|
|
|
|
public ExceptionDispatchInfo CapturedException;
|
|
public volatile JobExecutionStatus Status;
|
|
public volatile bool CancellationRequested;
|
|
|
|
public AsyncJobState(Action action, JobDeadlineMode deadline, string name)
|
|
{
|
|
Action = action;
|
|
Deadline = deadline;
|
|
Name = name;
|
|
Status = JobExecutionStatus.Queued;
|
|
}
|
|
|
|
public bool IsTerminal =>
|
|
Status == JobExecutionStatus.Completed ||
|
|
Status == JobExecutionStatus.Faulted ||
|
|
Status == JobExecutionStatus.Canceled;
|
|
|
|
public void Dispose()
|
|
{
|
|
Completion.Dispose();
|
|
}
|
|
}
|
|
|
|
public int WorkerCount => workers?.Length ?? 0;
|
|
public int AsyncWorkerCount => asyncWorkers?.Length ?? 0;
|
|
public bool IsInitialized => workers != null;
|
|
|
|
public void Initialize(int threadCount = -1, int asyncThreadCount = -1)
|
|
{
|
|
if (IsInitialized)
|
|
{
|
|
throw new InvalidOperationException("JobSystem is already initialized. Call Shutdown first.");
|
|
}
|
|
|
|
threadCount = threadCount < 0
|
|
? Math.Max(1, Environment.ProcessorCount - 2)
|
|
: Math.Max(1, threadCount);
|
|
|
|
running = true;
|
|
startSignal = new SemaphoreSlim(0);
|
|
completionEvent = new CountdownEvent(1);
|
|
asyncSignal = new SemaphoreSlim(0);
|
|
workers = new Thread[threadCount];
|
|
asyncQueue = new ConcurrentQueue<int>();
|
|
asyncJobStates = new ConcurrentDictionary<int, AsyncJobState>();
|
|
|
|
asyncThreadCount = asyncThreadCount < 0
|
|
? Math.Max(1, Environment.ProcessorCount >= 8 ? 2 : 1)
|
|
: Math.Max(0, asyncThreadCount);
|
|
|
|
asyncWorkers = new Thread[asyncThreadCount];
|
|
|
|
for (int i = 0; i < threadCount; i++)
|
|
{
|
|
workers[i] = new Thread(WorkerLoop)
|
|
{
|
|
IsBackground = true,
|
|
Name = $"Job-{i}",
|
|
};
|
|
workers[i].Start();
|
|
}
|
|
|
|
for (int i = 0; i < asyncThreadCount; i++)
|
|
{
|
|
asyncWorkers[i] = new Thread(AsyncWorkerLoop)
|
|
{
|
|
IsBackground = true,
|
|
Name = $"AsyncJob-{i}",
|
|
};
|
|
asyncWorkers[i].Start();
|
|
}
|
|
}
|
|
|
|
public JobHandle Submit(Action action, JobDeadlineMode deadline = JobDeadlineMode.Deferred, string name = "")
|
|
{
|
|
ArgumentNullException.ThrowIfNull(action);
|
|
|
|
if (!running)
|
|
{
|
|
throw new InvalidOperationException("JobSystem is not running. Call Initialize before submitting jobs.");
|
|
}
|
|
|
|
int id = Interlocked.Increment(ref nextAsyncJobId);
|
|
var state = new AsyncJobState(action, deadline, name ?? string.Empty);
|
|
|
|
if (!asyncJobStates.TryAdd(id, state))
|
|
{
|
|
state.Dispose();
|
|
throw new InvalidOperationException($"Failed to register async job {id}.");
|
|
}
|
|
|
|
asyncQueue.Enqueue(id);
|
|
asyncSignal.Release();
|
|
return new JobHandle(id);
|
|
}
|
|
|
|
public JobExecutionStatus GetStatus(JobHandle handle)
|
|
{
|
|
if (!handle.IsValid)
|
|
{
|
|
return JobExecutionStatus.Invalid;
|
|
}
|
|
|
|
return asyncJobStates.TryGetValue(handle.id, out AsyncJobState state)
|
|
? state.Status
|
|
: JobExecutionStatus.Invalid;
|
|
}
|
|
|
|
public bool IsCompleted(JobHandle handle)
|
|
{
|
|
return GetStatus(handle) is JobExecutionStatus.Completed or JobExecutionStatus.Faulted or JobExecutionStatus.Canceled;
|
|
}
|
|
|
|
public bool Cancel(JobHandle handle)
|
|
{
|
|
if (!handle.IsValid || !asyncJobStates.TryGetValue(handle.id, out AsyncJobState state))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
state.CancellationRequested = true;
|
|
return true;
|
|
}
|
|
|
|
public void Wait(JobHandle handle)
|
|
{
|
|
if (!handle.IsValid || !asyncJobStates.TryGetValue(handle.id, out AsyncJobState state))
|
|
{
|
|
throw new InvalidOperationException($"Unknown job handle {handle}.");
|
|
}
|
|
|
|
state.Completion.Wait();
|
|
|
|
if (state.Status == JobExecutionStatus.Faulted)
|
|
{
|
|
state.CapturedException?.Throw();
|
|
}
|
|
}
|
|
|
|
public bool TryForget(JobHandle handle)
|
|
{
|
|
if (!handle.IsValid || !asyncJobStates.TryGetValue(handle.id, out AsyncJobState state) || !state.IsTerminal)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (asyncJobStates.TryRemove(handle.id, out AsyncJobState removedState))
|
|
{
|
|
removedState.Dispose();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void Dispatch(int count, Action<int> action)
|
|
{
|
|
if (count <= 0) return;
|
|
|
|
if (!IsInitialized)
|
|
{
|
|
// Safe fallback: run inline if Initialize was never called.
|
|
for (int i = 0; i < count; i++)
|
|
action(i);
|
|
return;
|
|
}
|
|
|
|
currentAction = action;
|
|
workCount = count;
|
|
Volatile.Write(ref nextIndex, 0);
|
|
|
|
completionEvent.Reset(workers.Length);
|
|
startSignal.Release(workers.Length);
|
|
completionEvent.Wait();
|
|
|
|
currentAction = null;
|
|
}
|
|
|
|
public void Shutdown()
|
|
{
|
|
if (!IsInitialized) return;
|
|
|
|
running = false;
|
|
startSignal.Release(workers.Length); // wake all workers so they can see running=false and exit
|
|
|
|
foreach (Thread t in workers)
|
|
t.Join();
|
|
|
|
for (int i = 0; i < asyncWorkers.Length; i++)
|
|
{
|
|
asyncSignal.Release();
|
|
}
|
|
|
|
foreach (Thread t in asyncWorkers)
|
|
{
|
|
t.Join();
|
|
}
|
|
|
|
completionEvent.Dispose();
|
|
startSignal.Dispose();
|
|
asyncSignal.Dispose();
|
|
|
|
foreach (KeyValuePair<int, AsyncJobState> pair in asyncJobStates)
|
|
{
|
|
pair.Value.Dispose();
|
|
}
|
|
|
|
asyncJobStates.Clear();
|
|
|
|
asyncWorkers = null;
|
|
asyncQueue = null;
|
|
asyncJobStates = null;
|
|
workers = null;
|
|
}
|
|
|
|
public void Dispose() => Shutdown();
|
|
|
|
private void WorkerLoop()
|
|
{
|
|
while (true)
|
|
{
|
|
startSignal.Wait();
|
|
if (!running) return;
|
|
|
|
Action<int> action = currentAction;
|
|
int total = workCount;
|
|
|
|
while (true)
|
|
{
|
|
int index = Interlocked.Increment(ref nextIndex) - 1;
|
|
if (index >= total) break;
|
|
action(index);
|
|
}
|
|
|
|
completionEvent.Signal();
|
|
}
|
|
}
|
|
|
|
private void AsyncWorkerLoop()
|
|
{
|
|
while (true)
|
|
{
|
|
asyncSignal.Wait();
|
|
|
|
if (!running)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!asyncQueue.TryDequeue(out int jobId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!asyncJobStates.TryGetValue(jobId, out AsyncJobState state))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (state.CancellationRequested)
|
|
{
|
|
state.Status = JobExecutionStatus.Canceled;
|
|
state.Completion.Set();
|
|
continue;
|
|
}
|
|
|
|
state.Status = JobExecutionStatus.Running;
|
|
try
|
|
{
|
|
state.Action();
|
|
state.Status = state.CancellationRequested
|
|
? JobExecutionStatus.Canceled
|
|
: JobExecutionStatus.Completed;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
state.CapturedException = ExceptionDispatchInfo.Capture(ex);
|
|
state.Status = JobExecutionStatus.Faulted;
|
|
}
|
|
finally
|
|
{
|
|
state.Completion.Set();
|
|
}
|
|
}
|
|
}
|
|
}
|