Skip to content

Instantly share code, notes, and snippets.

@radleta
Created November 18, 2019 13:55
Show Gist options
  • Save radleta/037797d8d6ac3c792dc83183cceb6785 to your computer and use it in GitHub Desktop.
Save radleta/037797d8d6ac3c792dc83183cceb6785 to your computer and use it in GitHub Desktop.
Ensures exclusive execution of an async method.
using Nito.AsyncEx;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace RichardAdleta
{
/// <summary>
/// The cookie that holds an <see cref="Lock"/> with an <see cref="Completed"/>. The <see cref="Intialized"/> property
/// is set when <see cref="Completed"/> is already initailized.
/// </summary>
/// <typeparam name="TItem">The type of the item.</typeparam>
public class AsyncLockCookie<TResult>
{
/// <summary>
/// The internal sync lock to coordinate creation of the <see cref="_task"/>.
/// </summary>
private readonly object _syncLock = new object();
/// <summary>
/// The async method to use to create the item.
/// </summary>
private readonly Func<CancellationToken, Task<TResult>> _create;
/// <summary>
/// The item.
/// </summary>
public TaskCompletionSource<TResult> Completed { get; private set; } = new TaskCompletionSource<TResult>();
/// <summary>
/// Determines whether or not the underlying task has been initialized.
/// </summary>
public bool Initialized { get; private set; }
/// <summary>
/// Intializes a new instance.
/// </summary>
/// <param name="create">The factory method to create the <see cref="TResult"/>.</param>
public AsyncLockCookie(Func<CancellationToken, Task<TResult>> create)
{
_create = create ?? throw new ArgumentNullException(nameof(create));
}
/// <summary>
/// Gets the Item using the exclusive lock to initialize it when its not initialized.
/// </summary>
/// <param name="cancellationToken">Cancellation the operation.</param>
/// <returns>The awaitable task of the item.</returns>
public Task<TResult> GetExclusiveAsync(System.Threading.CancellationToken cancellationToken)
{
// when its already initialized then get it without the lock
if (Initialized)
{
return Completed.Task.WaitAsync(cancellationToken);
}
// lock it
var creator = false;
lock (_syncLock)
{
if (!Initialized)
{
Initialized = true;
creator = true;
}
}
if (creator)
{
return ExecuteAsync(cancellationToken);
}
return Completed.Task.WaitAsync(cancellationToken);
}
/// <summary>
/// Executes and sets the completion task.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<TResult> ExecuteAsync(System.Threading.CancellationToken cancellationToken)
{
try
{
var result = await _create(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
Completed.TrySetCanceled();
cancellationToken.ThrowIfCancellationRequested();
return result;
}
Completed.TrySetResult(result);
return result;
}
catch (Exception ex)
{
Completed.TrySetException(ex);
throw;
}
}
/// <summary>
/// Starts a separate task to execute the underlying task.
/// </summary>
/// <param name="cancellationToken">The cancellation token for the spawned task.</param>
public bool TryStart(System.Threading.CancellationToken cancellationToken)
{
if (Initialized)
{
return false;
}
var creator = false;
lock (_syncLock)
{
if (!Initialized)
{
Initialized = true;
creator = true;
}
}
if (creator)
{
_ = Task.Run(() => ExecuteAsync(cancellationToken), cancellationToken);
}
return creator;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment