// #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(); } } } }