tals
tals

Reputation: 191

Enforce an async method to be called once

Say I have a class that needs to perform some async initialization using an InitializeAsync() method. I want to make sure that the initialization is performed only once. If another thread calls this method while the initalization is already in progress, it will "await" until the first call returns.

I was thinking about the following imlementation (using SemaphoreSlim). Is there a better/simpler approach?

public class MyService : IMyService
{
    private readonly SemaphoreSlim mSemaphore = new SemaphoreSlim(1, 1);
    private bool mIsInitialized;

    public async Task InitializeAsync()
    {
        if (!mIsInitialized)
        {
            await mSemaphore.WaitAsync();

            if (!mIsInitialized)
            {
                await DoStuffOnlyOnceAsync();
                mIsInitialized = true;
            }

            mSemaphore.Release();
        }
    }

    private Task DoStuffOnlyOnceAsync()
    {
        return Task.Run(() =>
        {
            Thread.Sleep(10000);
        });
    }
}

Thanks!

Edit:

Since I'm using DI and this service will be injected, consuming it as a "Lazy" resource or using an async factory won't work for me (although it could be great in other use cases). Thus, the async initialization should be encapsulated within the class and transparent to the IMyService consumers.

The idea of wrapping the initialization code in a "dummy" AsyncLazy<> object will do the job, although it feels a bit unnatural to me.

Upvotes: 16

Views: 8043

Answers (5)

Christopher Hamkins
Christopher Hamkins

Reputation: 1649

If the initialization can take place in the constructor, then you can accomplish it by creating a Task method, start the Task in the constructor, and await the task in the method that needs to use its result. As @PauloMorgado mentions in https://stackoverflow.com/a/34039239/7424038, you can await a Task as many times as you like, so the calls to UseTheStuff() will all wait until the one and only Task has completed. Once it has actually completed, the await won't block and the method can proceed without actually waiting on the subsequent calls.

    public class MyService : IMyService
    {
        private readonly Task doStuffOnlyOnceTask;
        private InjectedDependency _dependency;

        MyService(InjectedDependency dependency) 
        {
            _dependency = dependency;
            doStuffOnlyOnceTask = DoStuffOnlyOnceAsync();
        }

        private async Task DoStuffOnlyOnceAsync()
        {
            await Task.Delay(10000);
        }

        public async Task<string> UseTheStuff()
        {
            await doStuffOnlyOnceTask;
            return $"{nameof(DoStuffOnlyOnceAsync)} ran once and completed.";
        }
    }

This is a relatively simple solution that doesn't require any other packages or classes.

Upvotes: 0

Theodor Zoulias
Theodor Zoulias

Reputation: 43901

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:

  1. 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 cached Task, so that the asynchronous operation can be retried. This makes the AsyncLazy<T> practically unusable for the purpose of implementing a caching system, for example.

  2. The asynchronous delegate is invoked on the ThreadPool. There is no way to invoke it on the calling thread.

  3. If we try to solve the previous problem by invoking the taskFactory delegate directly instead of wrapping it in Task.Factory.StartNew, then in the unfortunate case that the delegate blocks the calling thread for a significant amount of time, all threads that will await the AsyncLazy<T> instance will get blocked until the completion of the delegate. This is a direct consequence of how the Lazy<T> type works. This type was never designed for supporting asynchronous operations in any way.

  4. 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
        {
            Task<TResult> capturedTask = Volatile.Read(ref _task);
            if (capturedTask is not null) return capturedTask;

            Task<Task<TResult>> newTaskTask = new(_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.
                Task<TResult> 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() => this.Task.GetAwaiter();

    public ConfiguredTaskAwaitable<TResult> ConfigureAwait(
        bool continueOnCapturedContext)
        => this.Task.ConfigureAwait(continueOnCapturedContext);
}

Usage example:

AsyncLazy<string> lazyOperation = new(async () =>
{
    return await _httpClient.GetStringAsync("https://stackoverflow.com");
}, retryOnFailure: true);

//... (here 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 (lazyOperation = new(() => 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.

Improvement: It is possible to implement a more memory-efficient version of the AsyncLazy<TResult>, by making the retryOnFailure behavior mandatory instead of optional. In this case we could simply cache an unwrapped TResult value, instead of caching a fully fledged Task<TResult>. The optimized version can be found here.

Upvotes: 2

Yuval Itzchakov
Yuval Itzchakov

Reputation: 149628

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.

Upvotes: 15

i3arnon
i3arnon

Reputation: 116636

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.

Upvotes: 7

Stephen Cleary
Stephen Cleary

Reputation: 457302

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.

Upvotes: 6

Related Questions