You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

252 lines
9.9 KiB

  1. using System;
  2. using System.Collections;
  3. using System.Collections.Concurrent;
  4. using System.Collections.Generic;
  5. using System.Diagnostics;
  6. using System.Linq;
  7. using System.Text;
  8. using System.Threading;
  9. using System.Threading.Tasks;
  10. namespace IPA.Utilities.Async
  11. {
  12. /// <summary>
  13. /// A task scheduler that runs tasks on the Unity main thread via coroutines.
  14. /// </summary>
  15. public class UnityMainThreadTaskScheduler : TaskScheduler, IDisposable
  16. {
  17. /// <summary>
  18. /// Gets the default main thread scheduler that is managed by BSIPA.
  19. /// </summary>
  20. /// <value>a scheduler that is managed by BSIPA</value>
  21. public static new TaskScheduler Default { get; } = new UnityMainThreadTaskScheduler();
  22. /// <summary>
  23. /// Gets a factory for creating tasks on <see cref="Default"/>.
  24. /// </summary>
  25. /// <value>a factory for creating tasks on the default scheduler</value>
  26. public static TaskFactory Factory { get; } = new TaskFactory(Default);
  27. private readonly ConcurrentDictionary<QueueItem, Task> tasks = new ConcurrentDictionary<QueueItem, Task>();
  28. private int queueEndPosition = 0;
  29. private int queuePosition = 0;
  30. private struct QueueItem : IEquatable<int>, IEquatable<Task>, IEquatable<QueueItem>
  31. {
  32. public int Index;
  33. public Task Task;
  34. public QueueItem(int index, Task task) : this()
  35. {
  36. Index = index;
  37. Task = task;
  38. }
  39. public bool Equals(int other) => Index.Equals(other);
  40. public bool Equals(Task other) => Task.Equals(other);
  41. public bool Equals(QueueItem other) => other.Index == Index || other.Task == Task;
  42. }
  43. /// <summary>
  44. /// Gets whether or not this scheduler is currently executing tasks.
  45. /// </summary>
  46. /// <value><see langword="true"/> if the scheduler is running, <see langword="false"/> otherwise</value>
  47. public bool IsRunning { get; private set; } = false;
  48. /// <summary>
  49. /// Gets whether or not this scheduler is in the process of shutting down.
  50. /// </summary>
  51. /// <value><see langword="true"/> if the scheduler is shutting down, <see langword="false"/> otherwise</value>
  52. public bool Cancelling { get; private set; } = false;
  53. private int yieldAfterTasks = 64;
  54. /// <summary>
  55. /// Gets or sets the number of tasks to execute before yielding back to Unity.
  56. /// </summary>
  57. /// <value>the number of tasks to execute per resume</value>
  58. public int YieldAfterTasks
  59. {
  60. get => yieldAfterTasks;
  61. set
  62. {
  63. ThrowIfDisposed();
  64. if (value < 1)
  65. throw new ArgumentException("Value cannot be less than 1", nameof(value));
  66. yieldAfterTasks = value;
  67. }
  68. }
  69. private TimeSpan yieldAfterTime = TimeSpan.FromMilliseconds(.5); // auto-yield if more than half a millis has passed by default
  70. /// <summary>
  71. /// Gets or sets the amount of time to execute tasks for before yielding back to Unity. Default is 0.5ms.
  72. /// </summary>
  73. /// <value>the amount of time to execute tasks for before yielding back to Unity</value>
  74. public TimeSpan YieldAfterTime
  75. {
  76. get => yieldAfterTime;
  77. set
  78. {
  79. ThrowIfDisposed();
  80. if (value <= TimeSpan.Zero)
  81. throw new ArgumentException("Value must be greater than zero", nameof(value));
  82. yieldAfterTime = value;
  83. }
  84. }
  85. /// <summary>
  86. /// When used as a Unity coroutine, runs the scheduler. Otherwise, this is an invalid call.
  87. /// </summary>
  88. /// <remarks>
  89. /// <para>
  90. /// Do not ever call <see cref="UnityEngine.MonoBehaviour.StopCoroutine(IEnumerator)"/> on this
  91. /// coroutine, nor <see cref="UnityEngine.MonoBehaviour.StopAllCoroutines"/> on the behaviour hosting
  92. /// this coroutine. This has no way to detect this, and this object will become invalid.
  93. /// </para>
  94. /// <para>
  95. /// If you need to stop this coroutine, first call <see cref="Cancel"/>, then wait for it to
  96. /// exit on its own.
  97. /// </para>
  98. /// </remarks>
  99. /// <returns>a Unity coroutine</returns>
  100. /// <exception cref="ObjectDisposedException">if this scheduler is disposed</exception>
  101. /// <exception cref="InvalidOperationException">if the scheduler is already running</exception>
  102. public IEnumerator Coroutine()
  103. {
  104. ThrowIfDisposed();
  105. if (IsRunning)
  106. throw new InvalidOperationException("Scheduler already running");
  107. Cancelling = false;
  108. IsRunning = true;
  109. yield return null; // yield immediately
  110. var sw = new Stopwatch();
  111. try
  112. {
  113. while (!Cancelling)
  114. {
  115. if (queuePosition < queueEndPosition)
  116. {
  117. var yieldAfter = YieldAfterTasks;
  118. sw.Start();
  119. for (int i = 0; i < yieldAfter && queuePosition < queueEndPosition
  120. && sw.Elapsed < YieldAfterTime; i++)
  121. {
  122. if (tasks.TryRemove(new QueueItem { Index = Interlocked.Increment(ref queuePosition) }, out var task))
  123. TryExecuteTask(task); // we succesfully removed the task
  124. else
  125. i++; // we didn't
  126. }
  127. sw.Reset();
  128. }
  129. yield return null;
  130. }
  131. }
  132. finally
  133. {
  134. sw.Reset();
  135. IsRunning = false;
  136. }
  137. }
  138. /// <summary>
  139. /// Cancels the scheduler. If the scheduler is currently executing tasks, that batch will finish first.
  140. /// All remaining tasks will be left in the queue.
  141. /// </summary>
  142. /// <exception cref="ObjectDisposedException">if this scheduler is disposed</exception>
  143. /// <exception cref="InvalidOperationException">if the scheduler is not running</exception>
  144. public void Cancel()
  145. {
  146. ThrowIfDisposed();
  147. if (!IsRunning) throw new InvalidOperationException("The scheduler is not running");
  148. Cancelling = true;
  149. }
  150. /// <summary>
  151. /// Throws a <see cref="NotSupportedException"/>.
  152. /// </summary>
  153. /// <returns>nothing</returns>
  154. /// <exception cref="NotSupportedException">Always.</exception>
  155. protected override IEnumerable<Task> GetScheduledTasks()
  156. {
  157. // this is only for debuggers which we can't use sooooo
  158. throw new NotSupportedException();
  159. }
  160. /// <summary>
  161. /// Queues a given <see cref="Task"/> to this scheduler. The <see cref="Task"/> <i>must></i> be
  162. /// scheduled for this <see cref="TaskScheduler"/> by the runtime.
  163. /// </summary>
  164. /// <param name="task">the <see cref="Task"/> to queue</param>
  165. /// <exception cref="ObjectDisposedException">Thrown if this object has already been disposed.</exception>
  166. protected override void QueueTask(Task task)
  167. {
  168. ThrowIfDisposed();
  169. tasks.TryAdd(new QueueItem(Interlocked.Increment(ref queueEndPosition), task), task);
  170. }
  171. /// <summary>
  172. /// Rejects any attempts to execute a task inline.
  173. /// </summary>
  174. /// <remarks>
  175. /// This task scheduler <i>always</i> runs its tasks on the thread that it manages, therefore it doesn't
  176. /// make sense to run it inline.
  177. /// </remarks>
  178. /// <param name="task">the task to attempt to execute</param>
  179. /// <param name="taskWasPreviouslyQueued">whether the task was previously queued to this scheduler</param>
  180. /// <returns><see langword="false"/></returns>
  181. /// <exception cref="ObjectDisposedException">Thrown if this object has already been disposed.</exception>
  182. protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
  183. {
  184. ThrowIfDisposed();
  185. if (!UnityGame.OnMainThread) return false;
  186. if (taskWasPreviouslyQueued)
  187. if (!tasks.TryRemove(new QueueItem { Task = task }, out var _))
  188. return false; // if we couldn't remove it, its not in our queue, so it already ran
  189. return TryExecuteTask(task);
  190. }
  191. private void ThrowIfDisposed()
  192. {
  193. if (disposedValue)
  194. throw new ObjectDisposedException(nameof(SingleThreadTaskScheduler));
  195. }
  196. #region IDisposable Support
  197. private bool disposedValue = false; // To detect redundant calls
  198. /// <summary>
  199. /// Disposes this object.
  200. /// </summary>
  201. /// <param name="disposing">whether or not to dispose managed objects</param>
  202. protected virtual void Dispose(bool disposing)
  203. {
  204. if (!disposedValue)
  205. {
  206. if (disposing)
  207. {
  208. }
  209. disposedValue = true;
  210. }
  211. }
  212. /// <summary>
  213. /// Disposes this object. This puts the object into an unusable state.
  214. /// </summary>
  215. // This code added to correctly implement the disposable pattern.
  216. public void Dispose()
  217. {
  218. // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
  219. Dispose(true);
  220. }
  221. #endregion
  222. }
  223. }