2012-08-16

Asynchronous Lazy Initialization

When you have a lazy-created value, it's often useful to lazy-create it in an asynchronous fashion. A simple Lazy<T> provides lazy initialization, but the initialization executes synchronously when the value is created.

Stephen Toub first introduced this approach in a blog post way back in January, 2011. This is his code, which I've updated, documented, and tweaked slightly:

 
/// <summary>
/// Provides support for asynchronous lazy initialization. This type is fully threadsafe.
/// </summary>
/// <typeparam name="T">The type of object that is being asynchronously initialized.</typeparam>
public sealed class AsyncLazy<T>
{
    /// <summary>
    /// The underlying lazy task.
    /// </summary>
    private readonly Lazy<Task<T>> instance;

    /// <summary>
    /// Initializes a new instance of the <see cref="AsyncLazy&lt;T&gt;"/> class.
    /// </summary>
    /// <param name="factory">The delegate that is invoked on a background thread to produce the value when it is needed.</param>
    public AsyncLazy(Func<T> factory)
    {
        instance = new Lazy<Task<T>>(() => Task.Run(factory));
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="AsyncLazy&lt;T&gt;"/> class.
    /// </summary>
    /// <param name="factory">The asynchronous delegate that is invoked on a background thread to produce the value when it is needed.</param>
    public AsyncLazy(Func<Task<T>> factory)
    {
        instance = new Lazy<Task<T>>(() => Task.Run(factory));
    }

    /// <summary>
    /// Asynchronous infrastructure support. This method permits instances of <see cref="AsyncLazy&lt;T&gt;"/> to be await'ed.
    /// </summary>
    public TaskAwaiter<T> GetAwaiter()
    {
        return instance.Value.GetAwaiter();
    }

    /// <summary>
    /// Starts the asynchronous initialization, if it has not already started.
    /// </summary>
    public void Start()
    {
        var unused = instance.Value;
    }
}

The idea is to have a lazy-initialized task, which represents the initialization of the resource.

The factory delegate passed to the constructor can be either synchronous (Func<T>) or asynchronous (Func<Task<T>>); either way, it will be run on a thread pool thread. It will not be executed more than once, even when multiple threads attempt to start it simultaneously (this is guaranteed by the Lazy type).

There are two "triggers" which can start the initialization: awaiting an AsyncLazy<T> instance or explicitly calling Start. When the factory delegate completes, the value is available, and any methods awaiting the AsyncLazy<T> instance receive the value.

It takes a few minutes to wrap your head around the theory, but it's really easy in practice:

private static readonly AsyncLazy<MyResource> myResource = new AsyncLazy<MyResource>(
    () => new MyResource()
    // or:
    // async () => { var ret = new MyResource(); await ret.InitAsync(); return ret; }
);

...

public async Task UseResource()
{
  MyResource resource = await myResource;
  ...
}

Update, 2012-09-30

The AsyncLazy<T> type is now part of Nito.AsyncEx, which you can get via NuGet.

5 comments:

  1. What's the assignment in the Start() method for?
    ("var unused = instance.Value;")

    Does calling instance.Value trigger the lazy initialization?

    ReplyDelete
    Replies
    1. Yes. There are some use cases where you want to start the initialization but won't actually need the result until later.

      Of course, you can just do "var unused = myResource.Value;" yourself, but I think calling a method is cleaner.

      When you read "Value", you get back a Task which represents the factory delegate running on a thread pool thread. Accessing the "Value" property kicks off the factory delegate (if it's not already running).

      Delete
  2. Thanks! A couple of stray "span" tags in the code.

    ReplyDelete
  3. Very nice! What about a new one with INotifyPropertyChanged to notify when loaded(initialized) :-) !

    ReplyDelete
    Replies
    1. AsyncLazy is intended for use when your code awaits the asynchronous initialization. If you're doing data binding, use NotifyTaskCompletion instead:
      https://nitoasyncex.codeplex.com/wikipage?title=NotifyTaskCompletion
      http://blog.stephencleary.com/2013/01/async-oop-3-properties.html

      Delete