@@ -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