1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
|
// #define DEBUG_RWLOCK
using Unity.Jobs;
namespace Pathfinding.Jobs {
/// <summary>
/// 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 <see cref="Read"/> or <see cref="Write"/> on the lock and pass the returned JobHandle as a dependency the job, and then call <see cref="WriteLockAsync.UnlockAfter"/> 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 <see cref="ReadSync"/> and <see cref="WriteSync"/> methods. That's handled automatically for you.
///
/// See: https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock
///
/// <code>
/// var readLock = AstarPath.active.LockGraphDataForReading();
/// var handle = new MyJob {
/// // ...
/// }.Schedule(readLock.dependency);
/// readLock.UnlockAfter(handle);
/// </code>
/// </summary>
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
}
/// <summary>
/// Aquire a read lock on the main thread.
/// This method will block until all pending write locks have been released.
/// </summary>
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);
}
/// <summary>
/// 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.
///
/// <code>
/// var readLock = AstarPath.active.LockGraphDataForReading();
/// var handle = new MyJob {
/// // ...
/// }.Schedule(readLock.dependency);
/// readLock.UnlockAfter(handle);
/// </code>
/// </summary>
public ReadLockAsync Read () {
AddPendingAsync();
return new ReadLockAsync(this, lastWrite);
}
/// <summary>
/// Aquire a write lock on the main thread.
/// This method will block until all pending read and write locks have been released.
/// </summary>
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);
}
/// <summary>
/// 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.
///
/// <code>
/// var readLock = AstarPath.active.LockGraphDataForReading();
/// var handle = new MyJob {
/// // ...
/// }.Schedule(readLock.dependency);
/// readLock.UnlockAfter(handle);
/// </code>
/// </summary>
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);
}
/// <summary>Release the lock after the given job has completed</summary>
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;
}
/// <summary>Release the lock after the given job has completed</summary>
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;
}
/// <summary>Release the lock after the given job has completed</summary>
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;
}
/// <summary>Release the lock</summary>
public void Unlock () {
if (inner != null) inner.RemovePendingSync();
}
}
}
}
|