2023-04-24 15:49:03 +02:00

292 lines
7.5 KiB
C#

using System.Threading;
namespace Pathfinding {
/// <summary>Queue of paths to be processed by the system</summary>
class ThreadControlQueue {
public class QueueTerminationException : System.Exception {
}
Path head;
Path tail;
readonly System.Object lockObj = new System.Object();
readonly int numReceivers;
bool blocked;
/// <summary>
/// Number of receiver threads that are currently blocked.
/// This is only modified while a thread has a lock on lockObj
/// </summary>
int blockedReceivers;
/// <summary>
/// True while head == null.
/// This is only modified while a thread has a lock on lockObj
/// </summary>
bool starving;
/// <summary>
/// True after TerminateReceivers has been called.
/// All receivers will be terminated when they next call Pop.
/// </summary>
bool terminate;
ManualResetEvent block = new ManualResetEvent(true);
/// <summary>
/// Create a new queue with the specified number of receivers.
/// It is important that the number of receivers is fixed.
/// Properties like AllReceiversBlocked rely on knowing the exact number of receivers using the Pop (or PopNoBlock) methods.
/// </summary>
public ThreadControlQueue (int numReceivers) {
this.numReceivers = numReceivers;
}
/// <summary>True if the queue is empty</summary>
public bool IsEmpty {
get {
return head == null;
}
}
/// <summary>True if TerminateReceivers has been called</summary>
public bool IsTerminating {
get {
return terminate;
}
}
/// <summary>Block queue, all calls to Pop will block until Unblock is called</summary>
public void Block () {
lock (lockObj) {
blocked = true;
block.Reset();
}
}
/// <summary>
/// Unblock queue.
/// Calls to Pop will not block anymore.
/// See: Block
/// </summary>
public void Unblock () {
lock (lockObj) {
blocked = false;
block.Set();
}
}
/// <summary>
/// Aquires a lock on this queue.
/// Must be paired with a call to <see cref="Unlock"/>
/// </summary>
public void Lock () {
Monitor.Enter(lockObj);
}
/// <summary>Releases the lock on this queue</summary>
public void Unlock () {
Monitor.Exit(lockObj);
}
/// <summary>True if blocking and all receivers are waiting for unblocking</summary>
public bool AllReceiversBlocked {
get {
lock (lockObj) {
return blocked && blockedReceivers == numReceivers;
}
}
}
/// <summary>Push a path to the front of the queue</summary>
public void PushFront (Path path) {
lock (lockObj) {
// If termination is due, why add stuff to a queue which will not be read from anyway
if (terminate) return;
if (tail == null) {// (tail == null) ==> (head == null)
head = path;
tail = path;
if (starving && !blocked) {
starving = false;
block.Set();
} else {
starving = false;
}
} else {
path.next = head;
head = path;
}
}
}
/// <summary>Push a path to the end of the queue</summary>
public void Push (Path path) {
lock (lockObj) {
// If termination is due, why add stuff to a queue which will not be read from anyway
if (terminate) return;
if (tail == null) {// (tail == null) ==> (head == null)
head = path;
tail = path;
if (starving && !blocked) {
starving = false;
block.Set();
} else {
starving = false;
}
} else {
tail.next = path;
tail = path;
}
}
}
void Starving () {
starving = true;
block.Reset();
}
/// <summary>All calls to Pop and PopNoBlock will now generate exceptions</summary>
public void TerminateReceivers () {
lock (lockObj) {
terminate = true;
block.Set();
}
}
/// <summary>
/// Pops the next item off the queue.
/// This call will block if there are no items in the queue or if the queue is currently blocked.
///
/// Returns: A Path object, guaranteed to be not null.
/// \throws QueueTerminationException if <see cref="TerminateReceivers"/> has been called.
/// \throws System.InvalidOperationException if more receivers get blocked than the fixed count sent to the constructor
/// </summary>
public Path Pop () {
Monitor.Enter(lockObj);
try {
if (terminate) {
blockedReceivers++;
throw new QueueTerminationException();
}
if (head == null) {
Starving();
}
while (blocked || starving) {
blockedReceivers++;
if (blockedReceivers > numReceivers) {
throw new System.InvalidOperationException("More receivers are blocked than specified in constructor ("+blockedReceivers + " > " + numReceivers+")");
}
Monitor.Exit(lockObj);
block.WaitOne();
Monitor.Enter(lockObj);
if (terminate) {
throw new QueueTerminationException();
}
blockedReceivers--;
if (head == null) {
Starving();
}
}
Path p = head;
var newHead = head.next;
if (newHead == null) {
tail = null;
}
head.next = null;
head = newHead;
return p;
} finally {
// Normally this only exits via a QueueTerminationException and will always be entered in that case.
// However the thread may also be aborted using a ThreadAbortException which can happen at any time.
// In particular if the Unity Editor recompiles scripts and is configured to exit play mode on recompilation
// then it will apparently abort all threads before the AstarPath.OnDestroy method is called (which would have
// cleaned up the threads gracefully). So we need to check if we actually hold the lock before releaseing it.
if (Monitor.IsEntered(lockObj)) {
Monitor.Exit(lockObj);
}
}
}
/// <summary>
/// Call when a receiver was terminated in other ways than by a QueueTerminationException.
///
/// After this call, the receiver should be dead and not call anything else in this class.
/// </summary>
public void ReceiverTerminated () {
Monitor.Enter(lockObj);
blockedReceivers++;
Monitor.Exit(lockObj);
}
/// <summary>
/// Pops the next item off the queue, this call will not block.
/// To ensure stability, the caller must follow this pattern.
/// 1. Call PopNoBlock(false), if a null value is returned, wait for a bit (e.g yield return null in a Unity coroutine)
/// 2. try again with PopNoBlock(true), if still null, wait for a bit
/// 3. Repeat from step 2.
///
/// \throws QueueTerminationException if <see cref="TerminateReceivers"/> has been called.
/// \throws System.InvalidOperationException if more receivers get blocked than the fixed count sent to the constructor
/// </summary>
public Path PopNoBlock (bool blockedBefore) {
Monitor.Enter(lockObj);
try {
if (terminate) {
blockedReceivers++;
throw new QueueTerminationException();
}
if (head == null) {
Starving();
}
if (blocked || starving) {
if (!blockedBefore) {
blockedReceivers++;
if (terminate) throw new QueueTerminationException();
if (blockedReceivers == numReceivers) {
//Last alive
} else if (blockedReceivers > numReceivers) {
throw new System.InvalidOperationException("More receivers are blocked than specified in constructor ("+blockedReceivers + " > " + numReceivers+")");
}
}
return null;
}
if (blockedBefore) {
blockedReceivers--;
}
Path p = head;
var newHead = head.next;
if (newHead == null) {
tail = null;
}
head.next = null;
head = newHead;
return p;
} finally {
Monitor.Exit(lockObj);
}
}
}
}