Here is a component similar in shape with the AsyncLazy<T>
type (also available in the Nito.AsyncEx library by Stephen Cleary), that has a behavior tailored to your needs:
/// <summary>
/// Represents an asynchronous operation that is invoked lazily on demand, can be
/// invoked multiple times, and is subject to a non-concurrent execution policy.
/// Concurrent observers receive the result of the same operation.
/// </summary>
public class AsyncCollapseConcurrent
{
private readonly Func<Task> _taskFactory;
private volatile Task _task;
public AsyncCollapseConcurrent(Func<Task> taskFactory)
{
ArgumentNullException.ThrowIfNull(taskFactory);
_taskFactory = taskFactory;
}
public Task Task
{
get
{
Task capturedTask = _task;
if (capturedTask is not null) return capturedTask;
Task<Task> newTaskTask = new(_taskFactory);
Task newTask = newTaskTask.Unwrap().ContinueWith(t =>
{
_task = null;
return t;
}, default, TaskContinuationOptions.DenyChildAttach |
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default).Unwrap();
capturedTask = Interlocked
.CompareExchange(ref _task, newTask, null) ?? newTask;
if (ReferenceEquals(capturedTask, newTask))
newTaskTask.RunSynchronously(TaskScheduler.Default);
return capturedTask;
}
}
public TaskAwaiter GetAwaiter() => Task.GetAwaiter();
public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext)
=> Task.ConfigureAwait(continueOnCapturedContext);
}
Usage example:
private readonly AsyncCollapseConcurrent _asyncLazy;
//...
_asyncLazy = new(() => SlowRewriteFolderAsync());
//...
await _asyncLazy;
The AsyncCollapseConcurrent
ensures that the taskFactory
will not be invoked concurrently, by creating a cold nested Task<Task>
using the Task<T>
constructor, and starting this task only in case the atomic Interlocked.CompareExchange
operation succeeds. Otherwise, in case the race to update the _task
field is won by another thread, the current thread discards the cold Task<Task>
without starting it.
I have used this technique for implementing various AsyncLazy<T>
variants, like this (with retry) or this (with expiration).
In case your SlowRewriteFolderAsync
method returns a generic Task<TResult>
, you can find a compatible generic AsyncCollapseConcurrent<TResult>
class here.
>
– Rodrigo Salazar Aug 21 '22 at 22:08