Created
November 30, 2016 22:38
-
-
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#
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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