ThreadControlQueue.cs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. using System.Threading;
  2. namespace Pathfinding {
  3. /// <summary>Queue of paths to be processed by the system</summary>
  4. class ThreadControlQueue {
  5. public class QueueTerminationException : System.Exception {
  6. }
  7. Path head;
  8. Path tail;
  9. readonly System.Object lockObj = new System.Object();
  10. readonly int numReceivers;
  11. bool blocked;
  12. /// <summary>
  13. /// Number of receiver threads that are currently blocked.
  14. /// This is only modified while a thread has a lock on lockObj
  15. /// </summary>
  16. int blockedReceivers;
  17. /// <summary>
  18. /// True while head == null.
  19. /// This is only modified while a thread has a lock on lockObj
  20. /// </summary>
  21. bool starving;
  22. /// <summary>
  23. /// True after TerminateReceivers has been called.
  24. /// All receivers will be terminated when they next call Pop.
  25. /// </summary>
  26. bool terminate;
  27. ManualResetEvent block = new ManualResetEvent(true);
  28. /// <summary>
  29. /// Create a new queue with the specified number of receivers.
  30. /// It is important that the number of receivers is fixed.
  31. /// Properties like AllReceiversBlocked rely on knowing the exact number of receivers using the Pop (or PopNoBlock) methods.
  32. /// </summary>
  33. public ThreadControlQueue (int numReceivers) {
  34. this.numReceivers = numReceivers;
  35. }
  36. /// <summary>True if the queue is empty</summary>
  37. public bool IsEmpty {
  38. get {
  39. return head == null;
  40. }
  41. }
  42. /// <summary>True if TerminateReceivers has been called</summary>
  43. public bool IsTerminating {
  44. get {
  45. return terminate;
  46. }
  47. }
  48. /// <summary>Block queue, all calls to Pop will block until Unblock is called</summary>
  49. public void Block () {
  50. lock (lockObj) {
  51. blocked = true;
  52. block.Reset();
  53. }
  54. }
  55. /// <summary>
  56. /// Unblock queue.
  57. /// Calls to Pop will not block anymore.
  58. /// See: Block
  59. /// </summary>
  60. public void Unblock () {
  61. lock (lockObj) {
  62. blocked = false;
  63. block.Set();
  64. }
  65. }
  66. /// <summary>
  67. /// Aquires a lock on this queue.
  68. /// Must be paired with a call to <see cref="Unlock"/>
  69. /// </summary>
  70. public void Lock () {
  71. Monitor.Enter(lockObj);
  72. }
  73. /// <summary>Releases the lock on this queue</summary>
  74. public void Unlock () {
  75. Monitor.Exit(lockObj);
  76. }
  77. /// <summary>True if blocking and all receivers are waiting for unblocking</summary>
  78. public bool AllReceiversBlocked {
  79. get {
  80. lock (lockObj) {
  81. return blocked && blockedReceivers == numReceivers;
  82. }
  83. }
  84. }
  85. /// <summary>Push a path to the front of the queue</summary>
  86. public void PushFront (Path path) {
  87. lock (lockObj) {
  88. // If termination is due, why add stuff to a queue which will not be read from anyway
  89. if (terminate) return;
  90. if (tail == null) {// (tail == null) ==> (head == null)
  91. head = path;
  92. tail = path;
  93. if (starving && !blocked) {
  94. starving = false;
  95. block.Set();
  96. } else {
  97. starving = false;
  98. }
  99. } else {
  100. path.next = head;
  101. head = path;
  102. }
  103. }
  104. }
  105. /// <summary>Push a path to the end of the queue</summary>
  106. public void Push (Path path) {
  107. lock (lockObj) {
  108. // If termination is due, why add stuff to a queue which will not be read from anyway
  109. if (terminate) return;
  110. if (tail == null) {// (tail == null) ==> (head == null)
  111. head = path;
  112. tail = path;
  113. if (starving && !blocked) {
  114. starving = false;
  115. block.Set();
  116. } else {
  117. starving = false;
  118. }
  119. } else {
  120. tail.next = path;
  121. tail = path;
  122. }
  123. }
  124. }
  125. void Starving () {
  126. starving = true;
  127. block.Reset();
  128. }
  129. /// <summary>All calls to Pop and PopNoBlock will now generate exceptions</summary>
  130. public void TerminateReceivers () {
  131. lock (lockObj) {
  132. terminate = true;
  133. block.Set();
  134. }
  135. }
  136. /// <summary>
  137. /// Pops the next item off the queue.
  138. /// This call will block if there are no items in the queue or if the queue is currently blocked.
  139. ///
  140. /// Returns: A Path object, guaranteed to be not null.
  141. /// Throws: QueueTerminationException if <see cref="TerminateReceivers"/> has been called.
  142. /// Throws: System.InvalidOperationException if more receivers get blocked than the fixed count sent to the constructor
  143. /// </summary>
  144. public Path Pop () {
  145. Monitor.Enter(lockObj);
  146. try {
  147. if (terminate) {
  148. blockedReceivers++;
  149. throw new QueueTerminationException();
  150. }
  151. if (head == null) {
  152. Starving();
  153. }
  154. while (blocked || starving) {
  155. blockedReceivers++;
  156. if (blockedReceivers > numReceivers) {
  157. throw new System.InvalidOperationException("More receivers are blocked than specified in constructor ("+blockedReceivers + " > " + numReceivers+")");
  158. }
  159. Monitor.Exit(lockObj);
  160. block.WaitOne();
  161. Monitor.Enter(lockObj);
  162. if (terminate) {
  163. throw new QueueTerminationException();
  164. }
  165. blockedReceivers--;
  166. if (head == null) {
  167. Starving();
  168. }
  169. }
  170. Path p = head;
  171. var newHead = head.next;
  172. if (newHead == null) {
  173. tail = null;
  174. }
  175. head.next = null;
  176. head = newHead;
  177. return p;
  178. } finally {
  179. // Normally this only exits via a QueueTerminationException and will always be entered in that case.
  180. // However the thread may also be aborted using a ThreadAbortException which can happen at any time.
  181. // In particular if the Unity Editor recompiles scripts and is configured to exit play mode on recompilation
  182. // then it will apparently abort all threads before the AstarPath.OnDestroy method is called (which would have
  183. // cleaned up the threads gracefully). So we need to check if we actually hold the lock before releaseing it.
  184. if (Monitor.IsEntered(lockObj)) {
  185. Monitor.Exit(lockObj);
  186. }
  187. }
  188. }
  189. /// <summary>
  190. /// Call when a receiver was terminated in other ways than by a QueueTerminationException.
  191. ///
  192. /// After this call, the receiver should be dead and not call anything else in this class.
  193. /// </summary>
  194. public void ReceiverTerminated () {
  195. Monitor.Enter(lockObj);
  196. blockedReceivers++;
  197. Monitor.Exit(lockObj);
  198. }
  199. /// <summary>
  200. /// Pops the next item off the queue, this call will not block.
  201. /// To ensure stability, the caller must follow this pattern.
  202. /// 1. Call PopNoBlock(false), if a null value is returned, wait for a bit (e.g yield return null in a Unity coroutine)
  203. /// 2. try again with PopNoBlock(true), if still null, wait for a bit
  204. /// 3. Repeat from step 2.
  205. ///
  206. /// Throws: QueueTerminationException if <see cref="TerminateReceivers"/> has been called.
  207. /// Throws: System.InvalidOperationException if more receivers get blocked than the fixed count sent to the constructor
  208. /// </summary>
  209. public Path PopNoBlock (bool blockedBefore) {
  210. Monitor.Enter(lockObj);
  211. try {
  212. if (terminate) {
  213. blockedReceivers++;
  214. throw new QueueTerminationException();
  215. }
  216. if (head == null) {
  217. Starving();
  218. }
  219. if (blocked || starving) {
  220. if (!blockedBefore) {
  221. blockedReceivers++;
  222. if (terminate) throw new QueueTerminationException();
  223. if (blockedReceivers == numReceivers) {
  224. //Last alive
  225. } else if (blockedReceivers > numReceivers) {
  226. throw new System.InvalidOperationException("More receivers are blocked than specified in constructor ("+blockedReceivers + " > " + numReceivers+")");
  227. }
  228. }
  229. return null;
  230. }
  231. if (blockedBefore) {
  232. blockedReceivers--;
  233. }
  234. Path p = head;
  235. var newHead = head.next;
  236. if (newHead == null) {
  237. tail = null;
  238. }
  239. head.next = null;
  240. head = newHead;
  241. return p;
  242. } finally {
  243. Monitor.Exit(lockObj);
  244. }
  245. }
  246. }
  247. }