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.

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