Enforce an async method to be called once
I have a blog post that covers a few different options for doing "asynchronous constructors".
Normally, I prefer asynchronous factory methods, because I think they're simpler and a bit safer:
public class MyService
{
private MyService() { }
public static async Task<MyService> CreateAsync()
{
var result = new MyService();
result.Value = await ...;
return result;
}
}
AsyncLazy<T>
is a perfectly good way of defining a shared asynchronous resource (and may be a better conceptual match for a "service", depending on how it is used). The one advantage of the async factory method approach is that it's not possible to create an uninitialized version of MyService
.
Yes. Use Stephen Cleary's AsyncLazy
(available on the AsyncEx
nuget):
private static readonly AsyncLazy<MyResource> myResource = new AsyncLazy<MyResource>(
async () =>
{
var ret = new MyResource();
await ret.InitAsync();
return ret;
}
);
public async Task UseResource()
{
MyResource resource = await myResource;
// ...
}
Or the visual studio SDK's AsyncLazy
if you prefer a Microsoft implementation.
I'd go with AsyncLazy<T>
(slightly modified version):
public class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(Func<T> valueFactory) :
base(() => Task.Run(valueFactory)) { }
public AsyncLazy(Func<Task<T>> taskFactory) :
base(() => Task.Run(() => taskFactory())) { }
public TaskAwaiter<T> GetAwaiter() { return Value.GetAwaiter(); }
}
And consume it like this:
private AsyncLazy<bool> asyncLazy = new AsyncLazy<bool>(async () =>
{
await DoStuffOnlyOnceAsync()
return true;
});
Note i'm using bool
simply because you have no return type from DoStuffOnlyOnceAsync
.
Edit:
Stephan Cleary (of course) also has an implementation of this here.
Stephen Toub's AsyncLazy<T>
implementation, based on a Lazy<Task<T>>
, is pretty nice and concise, but there are a few things that are not entirely to my liking:
In case the asynchronous operation fails, the error is cached, and will be propagated to all future awaiters of the
AsyncLazy<T>
instance. There is no way to un-cache the cachedTask
, so that the asynchronous operation can be retried. This makes theAsyncLazy<T>
practically unusable for the purpose of implementing a caching system, for example.The asynchronous delegate is invoked on the
ThreadPool
. There is no way to invoke it on the calling thread.If we try to solve the previous problem by invoking the
taskFactory
delegate directly instead of wrapping it inTask.Factory.StartNew
, then in the unfortunate case that the delegate blocks the calling thread for a significant amount of time, all threads that willawait
theAsyncLazy<T>
instance will get blocked until the completion of the delegate. This is a direct consequence of how theLazy<T>
type works. This type was never designed for supporting asynchronous operations in any way.The
Lazy<Task<T>>
combination generates warnings in the latest version of the Visual Studio 2019 (16.8.2). It seems that this combination can produce deadlocks in some scenarios.
The first issue has been addressed by Stephen Cleary's AsyncLazy<T>
implementation (part of the AsyncEx library), that accepts a RetryOnFailure
flag in its constructor. The second issue has also been addressed by the same implementation (ExecuteOnCallingThread
flag). AFAIK the third and the fourth issues have not been addressed.
Below is an attempt to address all of these issues. This implementation instead of being based on a Lazy<Task<T>>
, it is based on a transient nested task (Task<Task<T>>
).
/// <summary>
/// Represents the result of an asynchronous operation that is invoked lazily
/// on demand, with the option to retry it as many times as needed until it
/// succeeds, while enforcing a non-overlapping execution policy.
/// </summary>
public class AsyncLazy<TResult>
{
private Func<Task<TResult>> _taskFactory;
private readonly bool _retryOnFailure;
private Task<TResult> _task;
public AsyncLazy(Func<Task<TResult>> taskFactory, bool retryOnFailure = false)
{
ArgumentNullException.ThrowIfNull(taskFactory);
_taskFactory = taskFactory;
_retryOnFailure = retryOnFailure;
}
public Task<TResult> Task
{
get
{
var capturedTask = Volatile.Read(ref _task);
if (capturedTask is not null) return capturedTask;
var newTaskTask = new Task<Task<TResult>>(_taskFactory);
Task<TResult> newTask = null;
newTask = newTaskTask.Unwrap().ContinueWith(task =>
{
if (task.IsCompletedSuccessfully || !_retryOnFailure)
{
_taskFactory = null; // No longer needed (let it get recycled)
return task;
}
// Discard the stored _task, to trigger a retry later.
var original = Interlocked.Exchange(ref _task, null);
Debug.Assert(ReferenceEquals(original, newTask));
return task;
}, 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<TResult> GetAwaiter() => Task.GetAwaiter();
public ConfiguredTaskAwaitable<TResult> ConfigureAwait(
bool continueOnCapturedContext)
=> Task.ConfigureAwait(continueOnCapturedContext);
}
Usage example:
var lazyOperation = new AsyncLazy<string>(async () =>
{
return await _httpClient.GetStringAsync("https://stackoverflow.com");
}, retryOnFailure: true);
//... (the operation has not started yet)
string html = await lazyOperation;
The taskFactory
delegate is invoked on the calling thread (the thread that calls the await lazyOperation
in the example above). If you prefer to invoke it on the ThreadPool
, you can either change the implementation and replace the RunSynchronously
with the Start
method, or wrap the taskFactory
in Task.Run
(new AsyncLazy<string>(() => Task.Run(async () =>
in the example above). Normally an asynchronous delegate is expected to return quickly, so invoking it on the calling thread shouldn't be a problem. As a bonus it opens the possibility of interacting with thread-affine components, like UI controls, from inside the delegate.
This implementation propagates all the exceptions that might be thrown by the taskFactory
delegate, not just the first one. This might be important in a few cases, like when the delegate returns directly a Task.WhenAll
task. To do this first store the AsyncLazy<T>.Task
in a variable, then await
the variable, and finally in the catch
block inspect the Exception.InnerExceptions
property of the variable.
An online demonstration of the AsyncLazy<T>
class can be found here. It demonstrates the behavior of the class when used by multiple concurrent workers, and the taskFactory
fails.