Skip to content

Commit bddbf6a

Browse files
committed
Remove _retry race between Dispose() and Lock/LockAsync()
In lieu of the missing async condition variable, use an async await to first wait on the semaphore to become available then release the lock we are holding before blocking on the stashed task for the async wait.
1 parent 20e012c commit bddbf6a

File tree

1 file changed

+28
-12
lines changed

1 file changed

+28
-12
lines changed

AsyncLock/AsyncLock.cs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,17 @@ internal async Task<IDisposable> ObtainLockAsync(CancellationToken ct = default)
7575
{
7676
break;
7777
}
78-
_parent._reentrancy.Release();
7978
// We need to wait for someone to leave the lock before trying again.
80-
await _parent._retry.WaitAsync(ct);
79+
// We need to "atomically" obtain _retry and release _reentrancy, but there
80+
// is no equivalent to a condition variable. Instead, we call *but don't await*
81+
// _retry.WaitAsync(), then release the reentrancy lock, *then* await the saved task.
82+
var waitTask = _parent._retry.WaitAsync(ct);
83+
_parent._reentrancy.Release();
84+
await waitTask;
8185
}
8286
// Reset the owning thread id after all await calls have finished, otherwise we
8387
// could be resumed on a different thread and set an incorrect value.
8488
_parent._owningThreadId = ThreadId;
85-
// In case of !synchronous and success, TryEnter() does not release the reentrancy lock
8689
_parent._reentrancy.Release();
8790
return this;
8891
}
@@ -98,7 +101,6 @@ internal async Task<IDisposable> ObtainLockAsync(CancellationToken ct = default)
98101
// Reset the owning thread id after all await calls have finished, otherwise we
99102
// could be resumed on a different thread and set an incorrect value.
100103
_parent._owningThreadId = ThreadId;
101-
// In case of !synchronous and success, TryEnter() does not release the reentrancy lock
102104
_parent._reentrancy.Release();
103105
return this;
104106
}
@@ -119,7 +121,6 @@ internal async Task<IDisposable> ObtainLockAsync(CancellationToken ct = default)
119121
// Reset the owning thread id after all await calls have finished, otherwise we
120122
// could be resumed on a different thread and set an incorrect value.
121123
_parent._owningThreadId = ThreadId;
122-
// In case of !synchronous and success, TryEnter() does not release the reentrancy lock
123124
_parent._reentrancy.Release();
124125
return this;
125126
}
@@ -128,7 +129,15 @@ internal async Task<IDisposable> ObtainLockAsync(CancellationToken ct = default)
128129
now = DateTimeOffset.UtcNow;
129130
remainder -= now - last;
130131
last = now;
131-
if (remainder < TimeSpan.Zero || !await _parent._retry.WaitAsync(remainder))
132+
if (remainder < TimeSpan.Zero)
133+
{
134+
_parent._reentrancy.Release();
135+
return null;
136+
}
137+
138+
var waitTask = _parent._retry.WaitAsync(remainder);
139+
_parent._reentrancy.Release();
140+
if (!await waitTask)
132141
{
133142
return null;
134143
}
@@ -153,8 +162,9 @@ internal async Task<IDisposable> ObtainLockAsync(CancellationToken ct = default)
153162
break;
154163
}
155164
// We need to wait for someone to leave the lock before trying again.
165+
var waitTask = _parent._retry.WaitAsync(cancel);
156166
_parent._reentrancy.Release();
157-
await _parent._retry.WaitAsync(cancel);
167+
await waitTask;
158168
}
159169
}
160170
catch (OperationCanceledException)
@@ -165,7 +175,6 @@ internal async Task<IDisposable> ObtainLockAsync(CancellationToken ct = default)
165175
// Reset the owning thread id after all await calls have finished, otherwise we
166176
// could be resumed on a different thread and set an incorrect value.
167177
_parent._owningThreadId = ThreadId;
168-
// In case of !synchronous and success, TryEnter() does not release the reentrancy lock
169178
_parent._reentrancy.Release();
170179
return this;
171180
}
@@ -180,9 +189,12 @@ internal IDisposable ObtainLock(CancellationToken cancellationToken)
180189
_parent._reentrancy.Release();
181190
break;
182191
}
183-
_parent._reentrancy.Release();
184192
// We need to wait for someone to leave the lock before trying again.
185-
_parent._retry.Wait(cancellationToken);
193+
var waitTask = _parent._retry.WaitAsync(cancellationToken);
194+
_parent._reentrancy.Release();
195+
// This should be safe since the task we are awaiting doesn't need to make progress
196+
// itself to complete - it will be completed by another thread altogether. cf SemaphoreSlim internals.
197+
waitTask.GetAwaiter().GetResult();
186198
}
187199
return this;
188200
}
@@ -215,12 +227,14 @@ internal IDisposable ObtainLock(CancellationToken cancellationToken)
215227
_parent._reentrancy.Release();
216228
return this;
217229
}
218-
_parent._reentrancy.Release();
219230

220231
now = DateTimeOffset.UtcNow;
221232
remainder -= now - last;
222233
last = now;
223-
if (!_parent._retry.Wait(remainder))
234+
235+
var waitTask = _parent._retry.WaitAsync(remainder);
236+
_parent._reentrancy.Release();
237+
if (!waitTask.GetAwaiter().GetResult())
224238
{
225239
return null;
226240
}
@@ -297,6 +311,8 @@ public void Dispose()
297311
}
298312
// We can't place this within the _reentrances == 0 block above because we might
299313
// still need to notify a parallel reentrant task to wake. I think.
314+
// This should not be a race condition since we only wait on _retry with _reentrancy locked,
315+
// then release _reentrancy so the Dispose() call can obtain it to signal _retry in a big hack.
300316
if (@this._parent._retry.CurrentCount == 0)
301317
{
302318
@this._parent._retry.Release();

0 commit comments

Comments
 (0)