Skip to content

Instantly share code, notes, and snippets.

@antiduh
Created November 30, 2016 22:38
Show Gist options
  • Save antiduh/f60fc7b30f9dc09060ee6940c1704b0d to your computer and use it in GitHub Desktop.
Save antiduh/f60fc7b30f9dc09060ee6940c1704b0d to your computer and use it in GitHub Desktop.
Educational demonstration of how to build a spinning mutex from simplier parts, in C#
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Test
{
public class UserMutex
{
private Queue<AutoResetEvent> waiters;
private Queue<AutoResetEvent> unusedWaiters;
/// <summary>
/// A lock target that is used for performing book-keeping while trying to acquire
/// or release the mutex.
/// </summary>
private int criticalSection;
/// <summary>
/// The mutex itself.
/// </summary>
private int mutex;
public UserMutex()
{
this.mutex = 0;
this.criticalSection = 0;
this.waiters = new Queue<AutoResetEvent>(5);
this.unusedWaiters = new Queue<AutoResetEvent>(5);
}
public void Acquire()
{
// Acquire the critical section for mutex bookkeeping.
AcquireCriticalSection();
AutoResetEvent waiter = GetWaitToken();
while( mutex == 1 )
{
// Someone else owns the mutex.
// While we're holding the critical section, add ourselves
// to a queue of waiters, release the lock, and sleep until someone
// pops us.
this.waiters.Enqueue( waiter );
ReleaseCriticalSection();
waiter.WaitOne();
// We woke back up. We don't yet have the critical section,
// so acquire it again and see if we can take ownership of the mutex.
AcquireCriticalSection();
}
// We're holding the critical section AND the mutex is 0. We won. Mark that we
// own it, release the critical section and return to the caller.
mutex++;
ReturnWaitToken( waiter );
ReleaseCriticalSection();
}
public void Release()
{
// - Acquire the critical section so we can do book keeping.
// - Release the mutex
// - Find out if there are any waiters
// - Pop one if there is.
AcquireCriticalSection();
mutex--;
if( this.waiters.Count > 0 )
{
AutoResetEvent waiter = this.waiters.Dequeue();
// A small performance improvement.
//
// Once we remove the waiter from the queue, no other releaser will see him after we
// release the critical section. However, by releasing the critical section before
// waking him up, he has a better chance of getting the critical section on the first
// shot, improving performance.
ReleaseCriticalSection();
waiter.Set();
}
else
{
ReleaseCriticalSection();
}
}
public void AcquireHybrid()
{
// Acquire the critical section for mutex bookkeeping.
AcquireCriticalSection();
// Try a spinwait to see if we can get the mutex.
SpinWait wait = new SpinWait();
while( true )
{
if( mutex == 0 )
{
// The only time in my entire life I have ever used a goto.
goto gotmutex;
}
if( wait.NextSpinWillYield )
{
break;
}
ReleaseCriticalSection();
wait.SpinOnce();
AcquireCriticalSection();
}
// At this point we're still holding the critical section;
// We might've left the above loop due to running out of attempts
// or because we actually found the mutex unlocked.
// The below while loop will catch that.
AutoResetEvent waiter = GetWaitToken();
while( mutex == 1 )
{
// Someone else owns the mutex.
// While we're holding the critical section, add ourselves
// to a queue of waiters, release the lock, and sleep until someone
// pops us.
this.waiters.Enqueue( waiter );
ReleaseCriticalSection();
waiter.WaitOne();
// We woke back up. We don't yet have the critical section,
// so acquire it again and see if we can take ownership of the mutex.
AcquireCriticalSection();
}
ReturnWaitToken( waiter );
// We're holding the critical section AND the mutex is 0. We won. Mark that we
// own it, release the critical section and return to the caller.
gotmutex:
mutex++;
ReleaseCriticalSection();
}
private AutoResetEvent GetWaitToken()
{
AutoResetEvent waiter;
if( this.unusedWaiters.Count == 0 )
{
waiter = new AutoResetEvent( false );
}
else
{
waiter = this.unusedWaiters.Dequeue();
}
return waiter;
}
private void ReturnWaitToken( AutoResetEvent waiter )
{
this.unusedWaiters.Enqueue( waiter );
}
/// <summary>
/// Prevents execution from continuing until the critical section variable is owned.
/// Guaranteed to issue a full memory barrier before returning.
/// </summary>
private void AcquireCriticalSection()
{
// Atomically test to see if the value is 0; if it is, we'll swap 1 into it's
// place, and our return value will be zero.
// If another thread attempts this at exactly the same time, they'll atomically
// see that one is already there, and won't swap, and will return '1'.
while( Interlocked.CompareExchange( ref this.criticalSection, 1, 0 ) == 1 )
{
Thread.Yield();
}
}
private void ReleaseCriticalSection()
{
if( Interlocked.CompareExchange( ref this.criticalSection, 0, 1 ) == 0 )
{
// We tried to unlock a critical section that we didn't own.
throw new AbandonedMutexException();
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment