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.

260 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. /// Runs the task inline if the current thread is the Unity main thread.
  178. /// </summary>
  179. /// <param name="task">the task to attempt to execute</param>
  180. /// <param name="taskWasPreviouslyQueued">whether the task was previously queued to this scheduler</param>
  181. /// <returns><see langword="false"/></returns>
  182. /// <exception cref="ObjectDisposedException">Thrown if this object has already been disposed.</exception>
  183. protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
  184. {
  185. ThrowIfDisposed();
  186. if (!UnityGame.OnMainThread) return false;
  187. if (taskWasPreviouslyQueued)
  188. {
  189. if (itemTable.TryGetValue(task, out var item))
  190. {
  191. if (!item.HasTask) return false;
  192. item.HasTask = false;
  193. }
  194. else return false; // if we couldn't remove it, its not in our queue, so it already ran
  195. }
  196. return TryExecuteTask(task);
  197. }
  198. private void ThrowIfDisposed()
  199. {
  200. if (disposedValue)
  201. throw new ObjectDisposedException(nameof(SingleThreadTaskScheduler));
  202. }
  203. #region IDisposable Support
  204. private bool disposedValue = false; // To detect redundant calls
  205. /// <summary>
  206. /// Disposes this object.
  207. /// </summary>
  208. /// <param name="disposing">whether or not to dispose managed objects</param>
  209. protected virtual void Dispose(bool disposing)
  210. {
  211. if (!disposedValue)
  212. {
  213. if (disposing)
  214. {
  215. }
  216. disposedValue = true;
  217. }
  218. }
  219. /// <summary>
  220. /// Disposes this object. This puts the object into an unusable state.
  221. /// </summary>
  222. // This code added to correctly implement the disposable pattern.
  223. public void Dispose()
  224. {
  225. // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
  226. Dispose(true);
  227. }
  228. #endregion
  229. }
  230. }