Skip to content

Instantly share code, notes, and snippets.

@akarnokd
Created May 16, 2018 10:33
Show Gist options
  • Save akarnokd/bb498ac53d498c75880064d30fd4b4f5 to your computer and use it in GitHub Desktop.
Save akarnokd/bb498ac53d498c75880064d30fd4b4f5 to your computer and use it in GitHub Desktop.
/// <summary>
/// Asynchronous lock with stateful actions to execute.
/// </summary>
internal sealed class AsyncLock<TState> : IDisposable
{
struct Work
{
internal TState state;
internal Action<TState> action;
}
private object guard = new object();
private Queue<Work> queue;
private bool isAcquired = false;
private bool hasFaulted = false;
/// <summary>
/// Queues the action for execution. If the caller acquires the lock and becomes the owner,
/// the queue is processed. If the lock is already owned, the action is queued and will get
/// processed by the owner.
/// </summary>
/// <param name="state">The state to pass to the given action.</param>
/// <param name="action">Action to queue for execution.</param>
/// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
public void Wait(TState state, Action<TState> action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
// allow one thread to update the state
lock (guard)
{
// if a previous action crashed, ignore any future actions
if (hasFaulted)
{
return;
}
// if the "lock" is busy, queue up the extra work
// otherwise there is no need to queue up "action"
if (isAcquired)
{
// create the queue if necessary
var q = queue;
if (q == null)
{
q = new Queue<Work>();
queue = q;
}
// enqueue the work
q.Enqueue(new Work { state = state, action = action });
return;
}
// indicate there is processing going on
isAcquired = true;
}
// if we get here, execute the "action" first
for (; ; )
{
try
{
action(state);
}
catch
{
// the execution failed, terminate this AsyncLock
lock (guard)
{
// throw away the queue
queue = null;
// report fault
hasFaulted = true;
}
throw;
}
// execution succeeded, let's see if more work has to be done
lock (guard)
{
var q = queue;
// either there is no queue yet or we run out of work
if (q == null || q.Count == 0)
{
// release the lock
isAcquired = false;
return;
}
// get the next work action
var work = q.Dequeue();
state = work.state;
action = work.action;
}
// loop back and execute the action
}
}
/// <summary>
/// Clears the work items in the queue and drops further work being queued.
/// </summary>
public void Dispose()
{
lock (guard)
{
queue = null;
hasFaulted = true;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment