using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace IPA.Utilities.Async { /// /// A task scheduler that runs tasks on the Unity main thread via coroutines. /// public class UnityMainThreadTaskScheduler : TaskScheduler, IDisposable { /// /// Gets the default main thread scheduler that is managed by BSIPA. /// /// a scheduler that is managed by BSIPA public static new TaskScheduler Default { get; } = new UnityMainThreadTaskScheduler(); private readonly ConcurrentDictionary tasks = new ConcurrentDictionary(); private int queueEndPosition = 0; private int queuePosition = 0; private struct QueueItem : IEquatable, IEquatable, IEquatable { public int Index; public Task Task; public QueueItem(int index, Task task) : this() { Index = index; Task = task; } public bool Equals(int other) => Index.Equals(other); public bool Equals(Task other) => Task.Equals(other); public bool Equals(QueueItem other) => other.Index == Index || other.Task == Task; } /// /// Gets whether or not this scheduler is currently executing tasks. /// /// if the scheduler is running, otherwise public bool IsRunning { get; private set; } = false; /// /// Gets whether or not this scheduler is in the process of shutting down. /// /// if the scheduler is shutting down, otherwise public bool Cancelling { get; private set; } = false; private int yieldAfterTasks = 64; /// /// Gets or sets the number of tasks to execute before yielding back to Unity. /// /// the number of tasks to execute per resume public int YieldAfterTasks { get => yieldAfterTasks; set { ThrowIfDisposed(); if (value < 1) throw new ArgumentException("Value cannot be less than 1", nameof(value)); yieldAfterTasks = value; } } private TimeSpan yieldAfterTime = TimeSpan.FromMilliseconds(.5); // auto-yield if more than half a millis has passed by default /// /// Gets or sets the amount of time to execute tasks for before yielding back to Unity. Default is 0.5ms. /// /// the amount of time to execute tasks for before yielding back to Unity public TimeSpan YieldAfterTime { get => yieldAfterTime; set { ThrowIfDisposed(); if (value <= TimeSpan.Zero) throw new ArgumentException("Value must be greater than zero", nameof(value)); yieldAfterTime = value; } } /// /// When used as a Unity coroutine, runs the scheduler. Otherwise, this is an invalid call. /// /// a Unity coroutine /// if this scheduler is disposed /// if the scheduler is already running public IEnumerator Coroutine() { ThrowIfDisposed(); if (IsRunning) throw new InvalidOperationException("Scheduler already running"); Cancelling = false; IsRunning = true; yield return null; // yield immediately var sw = new Stopwatch(); try { while (!Cancelling) { if (queuePosition < queueEndPosition) { var yieldAfter = YieldAfterTasks; sw.Start(); for (int i = 0; i < yieldAfter && queuePosition < queueEndPosition && sw.Elapsed < YieldAfterTime; i++) { if (tasks.TryRemove(new QueueItem { Index = Interlocked.Increment(ref queuePosition) }, out var task)) TryExecuteTask(task); // we succesfully removed the task else i++; // we didn't } sw.Reset(); } yield return null; } } finally { sw.Reset(); IsRunning = false; } } /// /// Cancels the scheduler. If the scheduler is currently executing tasks, that batch will finish first. /// All remaining tasks will be left in the queue. /// /// if this scheduler is disposed /// if the scheduler is not running public void Cancel() { ThrowIfDisposed(); if (!IsRunning) throw new InvalidOperationException("The scheduler is not running"); Cancelling = true; } /// /// Throws a . /// /// nothing /// Always. protected override IEnumerable GetScheduledTasks() { // this is only for debuggers which we can't use sooooo throw new NotSupportedException(); } /// /// Queues a given to this scheduler. The must> be /// scheduled for this by the runtime. /// /// the to queue /// Thrown if this object has already been disposed. protected override void QueueTask(Task task) { ThrowIfDisposed(); tasks.TryAdd(new QueueItem(Interlocked.Increment(ref queueEndPosition), task), task); } /// /// Rejects any attempts to execute a task inline. /// /// /// This task scheduler always runs its tasks on the thread that it manages, therefore it doesn't /// make sense to run it inline. /// /// the task to attempt to execute /// whether the task was previously queued to this scheduler /// /// Thrown if this object has already been disposed. protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { ThrowIfDisposed(); if (!UnityGame.OnMainThread) return false; if (taskWasPreviouslyQueued) if (!tasks.TryRemove(new QueueItem { Task = task }, out var _)) return false; // if we couldn't remove it, its not in our queue, so it already ran return TryExecuteTask(task); } private void ThrowIfDisposed() { if (disposedValue) throw new ObjectDisposedException(nameof(SingleThreadTaskScheduler)); } #region IDisposable Support private bool disposedValue = false; // To detect redundant calls protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { } disposedValue = true; } } // This code added to correctly implement the disposable pattern. public void Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(true); } #endregion } }