Async Lazy In C# – With Great Power Comes Great Responsibility

Async Lazy In C# – With Great Power Comes Great Responsibility

In the world of software development, performance and efficiency are key. As developers, we constantly strive to write code that not only solves the problem at hand but also does so in the most efficient way possible. One such efficiency technique in C# is the use of the Lazy<T> class for lazy initialization. This allows us to defer potentially slow initialization to a point in the execution more just-in-time for when we need to consume a value. But what if we could take this a step further and introduce asynchrony into the mix? Is there something that gives us async lazy functionality?

In this blog post, we’ll explore the concept of async lazy initialization in C#, using Lazy<Task<T>> to achieve this. So, whether you’re a seasoned C# programmer or a curious beginner, buckle up for an exciting journey into the world of async lazy initialization! As always, we’ll touch on the pros and cons of what we’re looking at today.

Understanding Lazy Initialization

Before we dive into the async world, let’s first understand what lazy initialization is. Lazy initialization is a programming technique where the initialization of an object or value is deferred until it is first accessed. This can be particularly useful when the creation of an object is costly (in terms of time or resources), and the object is not used immediately when the application starts. Without lazy initialization, startup times can grow needlessly out of control. If you’ve been checking out my content on plugin architectures or working with Autofac for dependency injection, you may have found your initialization code is growing in scope.

In C#, this is achieved using the Lazy<T> class. Here’s a simple example:

Lazy<MyClass> myObject = new Lazy<MyClass>(() => new());

In the above code snippet, myObject is a Lazy<MyClass> instance. The actual MyClass object will not be created until myObject.Value is accessed for the first time. This can significantly improve the startup performance of your application if MyClass is expensive to create and you access this property only right before you need it the first time. What’s more, the Lazy<T> type handles thread safety around the initialization!

You can check out this video for more examples:

The Need for Async Lazy Initialization

While Lazy<T> is a powerful tool, it doesn’t appear to support asynchronous initialization. This can be a problem when the initialization of the object involves I/O operations or other long-running tasks that would benefit from being run asynchronously. Blocking the main thread for such operations can lead to a poor user experience, as it can make your application unresponsive. Additionally, as async/await patterns continue to become more prevalent in .NET code bases, the desire to have async lazy initialization will continue to grow.

This is where Lazy<Task<T>> comes into play. By using Lazy<Task<T>>, we can achieve asynchronous lazy initialization. The Task<T> represents an asynchronous operation that returns a result. When combined with Lazy<T>, it allows the expensive operation to be run asynchronously and its result to be consumed when needed.

So does Lazy<T> support async lazy initialization out of the box? Technically, yes, but many of us never consider that we can just replace the type parameter here with a Task<T>! By using a task as the type for the lazy wrapper, we essentially get async lazy right out of the box! Many C# developers are already aware of this, but if you were like me, the answer was hiding right under our noses.

Introducing Lazy<Task<T>>

Let’s see how we can implement asynchronous lazy initialization using Lazy<Task<T>>. Here’s a simple example:

Lazy<Task<MyClass>> myObject = new Lazy<Task<MyClass>>(() => Task.Run(() => new MyClass()));

In the above code snippet, myObject is a Lazy<Task<MyClass>> instance. The Task.Run(() => new MyClass()) is an asynchronous operation that creates a new MyClass object. This operation will not be run until myObject.Value is accessed for the first time. Furthermore, because it’s wrapped in a Task, it will be run asynchronously.

And you can do more than just directly instantiate an object! Let’s look at this example:

public async Task<MyClass> CreateMyClassAsync()
{
  // simulate being busy!
  await Task.Delay(2000);
  return new MyClass();
}

Lazy<Task<MyClass>> myObject = new Lazy<Task<MyClass>>(CreateMyClassAsync);

The code above references an async/await method that aims to demonstrate you can technically have any async code path passed in. The first examples that just show an object’s constructor being called are a little bit contrived because that should ideally be nearly instantaneous. So with this example, hopefully you can start to see the potential with longer running operations.

Consuming Lazy<Task<T>>

To consume the result of Lazy<Task<T>>, we need to await the Task<T>:

MyClass result = await myObject.Value;

In the above code snippet, myObject.Value returns a Task<MyClass>. By awaiting this task, we can get the MyClass object once it’s ready. If the task has not yet been completed, this will asynchronously wait for the task to complete before continuing. This ensures that your application remains responsive, even if the initialization of MyClass takes a long time. This is assuming that the rest of your async/await pattern in your code is actually being done properly, of course!

You can watch this video for more details on this:

The Power of Async Lazy Initialization

Enjoyed this post so far? If you want to see more about the pros and cons of lazy async along with a nice wrapper, check out the full post!

For more insights into C# and software development in general, subscribe to my newsletter! You can also check out my YouTube channel. Stay lazy, and happy coding!

Did you find this article valuable?

Support Dev Leader by becoming a sponsor. Any amount is appreciated!