// #define DEBUG_RWLOCK
using Unity.Jobs;
namespace Pathfinding.Jobs {
///
/// A simple read/write lock for use with the Unity Job System.
///
/// The RW-lock makes the following assumptions:
/// - Only the main thread will call the methods on this lock.
/// - If jobs are to use locked data, you should call or on the lock and pass the returned JobHandle as a dependency the job, and then call on the lock object, with the newly scheduled job's handle.
/// - When taking a Read lock, you should only read data, but if you take a Write lock you may modify data.
/// - On the main thread, multiple synchronous write locks may be nested.
///
/// You do not need to care about dependencies when calling the and methods. That's handled automatically for you.
///
/// See: https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock
///
///
/// var readLock = AstarPath.active.LockGraphDataForReading();
/// var handle = new MyJob {
/// // ...
/// }.Schedule(readLock.dependency);
/// readLock.UnlockAfter(handle);
///
///
public class RWLock {
JobHandle lastWrite;
JobHandle lastRead;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
int heldSyncLocks;
bool pendingAsync;
#if DEBUG_RWLOCK
string pendingStackTrace;
#endif
void CheckPendingAsync () {
#if DEBUG_RWLOCK
if (pendingAsync) throw new System.InvalidOperationException("An async lock was previously aquired, but UnlockAfter was never called on it. The lock was aquired at\n" + pendingStackTrace + "\n\n");
#else
if (pendingAsync) throw new System.InvalidOperationException("An async lock was previously aquired, but UnlockAfter was never called on it.");
#endif
}
#endif
void AddPendingSync () {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
CheckPendingAsync();
#if DEBUG_RWLOCK
pendingStackTrace = System.Environment.StackTrace;
#endif
heldSyncLocks++;
#endif
}
void RemovePendingSync () {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
if (heldSyncLocks <= 0) throw new System.InvalidOperationException("Tried to unlock a lock which was not locked. Did you call Unlock twice?");
heldSyncLocks--;
#endif
}
void AddPendingAsync () {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
CheckPendingAsync();
#if DEBUG_RWLOCK
if (heldSyncWriteLocks > 0) throw new System.InvalidOperationException("A synchronous lock is already being held. You cannot lock it asynchronously at the same time. The sync lock was aquired at\n" + pendingStackTrace + "\n\n");
pendingStackTrace = System.Environment.StackTrace;
#else
if (heldSyncLocks > 0) throw new System.InvalidOperationException("A synchronous lock is already being held. You cannot lock it asynchronously at the same time.");
#endif
pendingAsync = true;
#endif
}
void RemovePendingAsync () {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
pendingAsync = false;
#endif
}
///
/// Aquire a read lock on the main thread.
/// This method will block until all pending write locks have been released.
///
public LockSync ReadSync () {
AddPendingSync();
lastWrite.Complete();
lastWrite = default; // Setting this to default will avoid a call into unity's c++ parts next time we call Complete (improves perf slightly)
return new LockSync(this);
}
///
/// Aquire a read lock on the main thread.
/// This method will not block until all asynchronous write locks have been released, instead you should make sure to add the returned JobHandle as a dependency to any jobs that use the locked data.
///
/// If a synchronous write lock is currently held, this method will throw an exception.
///
///
/// var readLock = AstarPath.active.LockGraphDataForReading();
/// var handle = new MyJob {
/// // ...
/// }.Schedule(readLock.dependency);
/// readLock.UnlockAfter(handle);
///
///
public ReadLockAsync Read () {
AddPendingAsync();
return new ReadLockAsync(this, lastWrite);
}
///
/// Aquire a write lock on the main thread.
/// This method will block until all pending read and write locks have been released.
///
public LockSync WriteSync () {
AddPendingSync();
lastWrite.Complete();
lastWrite = default; // Setting this to default will avoid a call into unity's c++ parts next time we call Complete (improves perf slightly)
lastRead.Complete();
return new LockSync(this);
}
///
/// Aquire a write lock on the main thread.
/// This method will not block until all asynchronous read and write locks have been released, instead you should make sure to add the returned JobHandle as a dependency to any jobs that use the locked data.
///
/// If a synchronous write lock is currently held, this method will throw an exception.
///
///
/// var readLock = AstarPath.active.LockGraphDataForReading();
/// var handle = new MyJob {
/// // ...
/// }.Schedule(readLock.dependency);
/// readLock.UnlockAfter(handle);
///
///
public WriteLockAsync Write () {
AddPendingAsync();
return new WriteLockAsync(this, JobHandle.CombineDependencies(lastRead, lastWrite));
}
public readonly struct CombinedReadLockAsync {
readonly RWLock lock1;
readonly RWLock lock2;
public readonly JobHandle dependency;
public CombinedReadLockAsync(ReadLockAsync lock1, ReadLockAsync lock2) {
this.lock1 = lock1.inner;
this.lock2 = lock2.inner;
dependency = JobHandle.CombineDependencies(lock1.dependency, lock2.dependency);
}
/// Release the lock after the given job has completed
public void UnlockAfter (JobHandle handle) {
if (lock1 != null) {
lock1.RemovePendingAsync();
lock1.lastRead = JobHandle.CombineDependencies(lock1.lastRead, handle);
}
if (lock2 != null) {
lock2.RemovePendingAsync();
lock2.lastRead = JobHandle.CombineDependencies(lock2.lastRead, handle);
}
}
}
public readonly struct ReadLockAsync {
internal readonly RWLock inner;
public readonly JobHandle dependency;
public ReadLockAsync(RWLock inner, JobHandle dependency) {
this.inner = inner;
this.dependency = dependency;
}
/// Release the lock after the given job has completed
public void UnlockAfter (JobHandle handle) {
if (inner != null) {
inner.RemovePendingAsync();
inner.lastRead = JobHandle.CombineDependencies(inner.lastRead, handle);
}
}
}
public readonly struct WriteLockAsync {
readonly RWLock inner;
public readonly JobHandle dependency;
public WriteLockAsync(RWLock inner, JobHandle dependency) {
this.inner = inner;
this.dependency = dependency;
}
/// Release the lock after the given job has completed
public void UnlockAfter (JobHandle handle) {
if (inner != null) {
inner.RemovePendingAsync();
inner.lastWrite = handle;
}
}
}
public readonly struct LockSync {
readonly RWLock inner;
public LockSync(RWLock inner) {
this.inner = inner;
}
/// Release the lock
public void Unlock () {
if (inner != null) inner.RemovePendingSync();
}
}
}
}