What is the proper way to ensure that only the 'last-in' thread is given access to a mutex/locked region while intermediary threads do not acquire the lock?
Example sequence:
A acquires lock
B waits
C waits
B fails to acquire lock*
A releases lock
C acquires lock
*B should fail to acquire the lock either via an exception (as in SemaphoreSlim.Wait(CancellationToken)
or a boolean Monitor.TryEnter()
type construct.
I can think of several similar schemes to achieve this (such as using a CancellationTokenSource
and SemaphoreSlim
), but none of them seem particularly elegant.
Is there a common practice for this scenario?
This should work like you want, it uses a SemaphoreSlim with a size of 1 to control it. I also added support for passing in a CancelationToken to cancel waiting for the lock early, it also supports WaitAsync
returning a task instead of blocking.
public sealed class LastInLocker : IDisposable
{
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);
private CancellationTokenSource _cts = new CancellationTokenSource();
private bool _disposed = false;
public void Wait()
{
Wait(CancellationToken.None);
}
public void Wait(CancellationToken earlyCancellationToken)
{
if(_disposed)
throw new ObjectDisposedException("LastInLocker");
var token = ReplaceTokenSource(earlyCancellationToken);
_semaphore.Wait(token);
}
public Task WaitAsync()
{
return WaitAsync(CancellationToken.None);
}
public async Task WaitAsync(CancellationToken earlyCancellationToken)
{
if (_disposed)
throw new ObjectDisposedException("LastInLocker");
var token = ReplaceTokenSource(earlyCancellationToken);
//I await here because if ReplaceTokenSource thows a exception I want the
//observing of that exception to be deferred until the caller awaits my
//returned task.
await _semaphore.WaitAsync(token).ConfigureAwait(false);
}
public void Release()
{
if (_disposed)
throw new ObjectDisposedException("LastInLocker");
_semaphore.Release();
}
private CancellationToken ReplaceTokenSource(CancellationToken earlyCancellationToken)
{
var newSource = CancellationTokenSource.CreateLinkedTokenSource(earlyCancellationToken);
var oldSource = Interlocked.Exchange(ref _cts, newSource);
oldSource.Cancel();
oldSource.Dispose();
return newSource.Token;
}
public void Dispose()
{
_disposed = true;
_semaphore.Dispose();
_cts.Dispose();
}
}
Here is a little test program that re-creates your test example
internal class Program
{
static LastInLocker locker = new LastInLocker();
private static void Main(string[] args)
{
Task.Run(() => Test("A"));
Thread.Sleep(500);
Task.Run(() => Test("B"));
Thread.Sleep(500);
Task.Run(() => Test("C"));
Console.ReadLine();
}
private static void Test(string name)
{
Console.WriteLine("{0} waits for lock", name);
try
{
locker.Wait();
Console.WriteLine("{0} acquires lock", name);
Thread.Sleep(4000);
locker.Release();
Console.WriteLine("{0} releases lock", name);
}
catch (Exception)
{
Console.WriteLine("{0} fails to acquire lock", name);
}
}
}
outputs
A waits for lock A acquires lock B waits for lock C waits for lock B fails to acquire lock A releases lock C acquires lock C releases lock
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With