Browse Source

Documented UnityMainThreadTaskScheduler

pull/46/head
Anairkoen Schno 4 years ago
parent
commit
ae40ae7abc
1 changed files with 101 additions and 4 deletions
  1. +101
    -4
      IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs

+ 101
- 4
IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs View File

@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
@ -9,8 +10,15 @@ using System.Threading.Tasks;
namespace IPA.Utilities.Async
{
/// <summary>
/// A task scheduler that runs tasks on the Unity main thread via coroutines.
/// </summary>
public class UnityMainThreadTaskScheduler : TaskScheduler, IDisposable
{
/// <summary>
/// Gets the default main thread scheduler that is managed by BSIPA.
/// </summary>
/// <value>a scheduler that is managed by BSIPA</value>
public static new TaskScheduler Default { get; } = new UnityMainThreadTaskScheduler();
private readonly ConcurrentDictionary<QueueItem, Task> tasks = new ConcurrentDictionary<QueueItem, Task>();
@ -33,41 +41,113 @@ namespace IPA.Utilities.Async
public bool Equals(QueueItem other) => other.Index == Index || other.Task == Task;
}
/// <summary>
/// Gets whether or not this scheduler is currently executing tasks.
/// </summary>
/// <value><see langword="true"/> if the scheduler is running, <see langword="false"/> otherwise</value>
public bool IsRunning { get; private set; } = false;
public int YieldAfterTasks { get; set; } = 4;
/// <summary>
/// Gets whether or not this scheduler is in the process of shutting down.
/// </summary>
/// <value><see langword="true"/> if the scheduler is shutting down, <see langword="false"/> otherwise</value>
public bool Cancelling { get; private set; } = false;
private int yieldAfterTasks = 64;
/// <summary>
/// Gets or sets the number of tasks to execute before yielding back to Unity.
/// </summary>
/// <value>the number of tasks to execute per resume</value>
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
/// <summary>
/// Gets or sets the amount of time to execute tasks for before yielding back to Unity. Default is 0.5ms.
/// </summary>
/// <value>the amount of time to execute tasks for before yielding back to Unity</value>
public TimeSpan YieldAfterTime
{
get => yieldAfterTime;
set
{
ThrowIfDisposed();
if (value <= TimeSpan.Zero)
throw new ArgumentException("Value must be greater than zero", nameof(value));
yieldAfterTime = value;
}
}
/// <summary>
/// When used as a Unity coroutine, runs the scheduler. Otherwise, this is an invalid call.
/// </summary>
/// <returns>a Unity coroutine</returns>
/// <exception cref="ObjectDisposedException">if this scheduler is disposed</exception>
/// <exception cref="InvalidOperationException">if the scheduler is already running</exception>
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 (true)
while (!Cancelling)
{
if (queuePosition < queueEndPosition)
{
var yieldAfter = YieldAfterTasks;
for (int i = 0; i < yieldAfter && queuePosition < queueEndPosition; i++)
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;
}
}
/// <summary>
/// Cancels the scheduler. If the scheduler is currently executing tasks, that batch will finish first.
/// All remaining tasks will be left in the queue.
/// </summary>
/// <exception cref="ObjectDisposedException">if this scheduler is disposed</exception>
/// <exception cref="InvalidOperationException">if the scheduler is not running</exception>
public void Cancel()
{
ThrowIfDisposed();
if (!IsRunning) throw new InvalidOperationException("The scheduler is not running");
Cancelling = true;
}
/// <summary>
/// Throws a <see cref="NotSupportedException"/>.
/// </summary>
@ -79,6 +159,12 @@ namespace IPA.Utilities.Async
throw new NotSupportedException();
}
/// <summary>
/// Queues a given <see cref="Task"/> to this scheduler. The <see cref="Task"/> <i>must></i> be
/// scheduled for this <see cref="TaskScheduler"/> by the runtime.
/// </summary>
/// <param name="task">the <see cref="Task"/> to queue</param>
/// <exception cref="ObjectDisposedException">Thrown if this object has already been disposed.</exception>
protected override void QueueTask(Task task)
{
ThrowIfDisposed();
@ -86,6 +172,17 @@ namespace IPA.Utilities.Async
tasks.TryAdd(new QueueItem(Interlocked.Increment(ref queueEndPosition), task), task);
}
/// <summary>
/// Rejects any attempts to execute a task inline.
/// </summary>
/// <remarks>
/// This task scheduler <i>always</i> runs its tasks on the thread that it manages, therefore it doesn't
/// make sense to run it inline.
/// </remarks>
/// <param name="task">the task to attempt to execute</param>
/// <param name="taskWasPreviouslyQueued">whether the task was previously queued to this scheduler</param>
/// <returns><see langword="false"/></returns>
/// <exception cref="ObjectDisposedException">Thrown if this object has already been disposed.</exception>
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
ThrowIfDisposed();
@ -114,7 +211,7 @@ namespace IPA.Utilities.Async
{
if (disposing)
{
// TODO: dispose managed state (managed objects).
}
disposedValue = true;


Loading…
Cancel
Save