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