292 lines
7.5 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|