Skip to content

Commit bbed451

Browse files
committed
optimize async path for immediate acquisition
1 parent 422efab commit bbed451

2 files changed

Lines changed: 46 additions & 20 deletions

File tree

src/StackExchange.Redis/AwaitableMutex.netfx.cs

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,18 +97,44 @@ public bool TryTakeSync()
9797
}
9898

9999
public ValueTask<bool> TryTakeAsync(CancellationToken cancellationToken)
100+
{
101+
bool lockTaken = false;
102+
try
103+
{
104+
// try to acquire uncontested lock - that way we can avoid allocating the pending caller
105+
Monitor.TryEnter(_queue, 0, ref lockTaken);
106+
if (lockTaken)
107+
{
108+
if (_isDisposed) return DisposedAsync();
109+
if (cancellationToken.IsCancellationRequested)
110+
{
111+
return CanceledAsync(cancellationToken);
112+
}
113+
114+
if (TryTakeInsideLockCore()) return new ValueTask<bool>(true);
115+
}
116+
}
117+
finally
118+
{
119+
if (lockTaken) Monitor.Exit(_queue);
120+
}
121+
122+
return TryTakeAsyncSlow(cancellationToken);
123+
}
124+
125+
private ValueTask<bool> TryTakeAsyncSlow(CancellationToken cancellationToken)
100126
{
101127
lock (_queue)
102128
{
129+
if (_isDisposed) return DisposedAsync();
103130
if (cancellationToken.IsCancellationRequested)
104131
{
105-
ThrowIfDisposed();
106-
return AsyncPendingCaller.Canceled();
132+
return CanceledAsync(cancellationToken);
107133
}
108134

109-
if (TryTakeInsideLock()) return new ValueTask<bool>(true);
135+
if (TryTakeInsideLockCore()) return new ValueTask<bool>(true);
110136
if (TimeoutMilliseconds == 0) return new ValueTask<bool>(false);
111-
if (cancellationToken.IsCancellationRequested) return AsyncPendingCaller.Canceled();
137+
if (cancellationToken.IsCancellationRequested) return CanceledAsync(cancellationToken);
112138

113139
var pending = new AsyncPendingCaller(TimeoutMilliseconds, cancellationToken);
114140
_queue.Enqueue(pending);
@@ -137,6 +163,11 @@ public void Release()
137163
private bool TryTakeInsideLock()
138164
{
139165
ThrowIfDisposed();
166+
return TryTakeInsideLockCore();
167+
}
168+
169+
private bool TryTakeInsideLockCore()
170+
{
140171
if (_isHeld || _queue.Count != 0) return false;
141172
_isHeld = true;
142173
return true;
@@ -191,6 +222,12 @@ private void ThrowIfDisposed()
191222

192223
private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(AwaitableMutex));
193224

225+
private static ValueTask<bool> DisposedAsync()
226+
=> new(Task.FromException<bool>(new ObjectDisposedException(nameof(AwaitableMutex))));
227+
228+
private static ValueTask<bool> CanceledAsync(CancellationToken cancellationToken)
229+
=> new(Task.FromCanceled<bool>(cancellationToken));
230+
194231
private static uint GetTime() => (uint)Environment.TickCount;
195232

196233
private static int GetRemainingTimeout(uint startTime, int originalTimeoutMilliseconds)
@@ -280,15 +317,16 @@ private sealed class AsyncPendingCaller : TaskCompletionSource<bool>, IPendingCa
280317
{
281318
private static readonly TimerCallback s_onTimeout = state => ((AsyncPendingCaller)state!).TryComplete(CompletionState.TimedOut);
282319
private static readonly Action<object?> s_onCanceled = state => ((AsyncPendingCaller)state!).TryComplete(CompletionState.Canceled);
283-
private static readonly Task<bool> s_canceledTask = CreateCanceledTask();
284320

321+
private readonly CancellationToken _cancellationToken;
285322
private readonly CancellationTokenRegistration _cancellation;
286323
private readonly Timer? _timeout;
287324
private int _completionState;
288325

289326
public AsyncPendingCaller(int timeoutMilliseconds, CancellationToken cancellationToken)
290327
: base(TaskCreationOptions.RunContinuationsAsynchronously)
291328
{
329+
_cancellationToken = cancellationToken;
292330
if (timeoutMilliseconds != Timeout.Infinite)
293331
{
294332
_timeout = new Timer(s_onTimeout, this, timeoutMilliseconds, Timeout.Infinite);
@@ -300,8 +338,6 @@ public AsyncPendingCaller(int timeoutMilliseconds, CancellationToken cancellatio
300338
}
301339
}
302340

303-
public static ValueTask<bool> Canceled() => new(s_canceledTask);
304-
305341
public bool TryGrant() => TryComplete(CompletionState.Granted);
306342

307343
public void Abort() => TryComplete(CompletionState.Disposed);
@@ -331,21 +367,14 @@ private void Complete(CompletionState completionState)
331367
TrySetResult(false);
332368
break;
333369
case CompletionState.Canceled:
334-
TrySetCanceled();
370+
TrySetCanceled(_cancellationToken);
335371
break;
336372
case CompletionState.Disposed:
337373
TrySetException(new ObjectDisposedException(nameof(AwaitableMutex)));
338374
break;
339375
}
340376
}
341377

342-
private static Task<bool> CreateCanceledTask()
343-
{
344-
var source = new TaskCompletionSource<bool>();
345-
source.SetCanceled();
346-
return source.Task;
347-
}
348-
349378
private enum CompletionState
350379
{
351380
Pending = 0,

tests/StackExchange.Redis.Tests/AwaitableMutexTests.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public async Task AsyncCallerTimesOutWhileHeld()
5555
}
5656

5757
[Fact]
58-
public void DisposalPreventsNewAcquisitions()
58+
public async Task DisposalPreventsNewAcquisitions()
5959
{
6060
var mutex = AwaitableMutex.Create(timeoutMilliseconds: 100);
6161
Assert.True(mutex.TryTakeInstant());
@@ -65,10 +65,7 @@ public void DisposalPreventsNewAcquisitions()
6565
Assert.False(mutex.IsAvailable);
6666
Assert.Throws<ObjectDisposedException>(() => mutex.TryTakeInstant());
6767
Assert.Throws<ObjectDisposedException>(() => mutex.TryTakeSync());
68-
Assert.Throws<ObjectDisposedException>(() =>
69-
{
70-
_ = mutex.TryTakeAsync();
71-
});
68+
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await mutex.TryTakeAsync().AsTask());
7269
Assert.Throws<ObjectDisposedException>(() => mutex.Release());
7370
}
7471

0 commit comments

Comments
 (0)