paint-brush
Exploring Async Laziness in C#: Balancing Power with Responsibilityby@devleader
3,204 reads
3,204 reads

Exploring Async Laziness in C#: Balancing Power with Responsibility

by Dev LeaderAugust 14th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In C#, how can we balance asynchrony and laziness? Is there such a thing as async lazy? Let's explore our options built into dotnet!
featured image - Exploring Async Laziness in C#: Balancing Power with Responsibility
Dev Leader HackerNoon profile picture


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 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.


Content Overview

  • Understanding Lazy Initialization
  • The Need for Async Lazy Initialization
  • Introducing Lazy<Task<T>>
  • Consuming Lazy<Task<T>>
  • The Power of Async Lazy Initialization
  • Async Lazy Considerations
  • Stephen Toub’s AsyncLazy<T>
  • Conclusion



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

Async lazy initialization can be a powerful tool in your C# development toolkit if you weren’t already using Lazy<T> this way. It combines the benefits of lazy initialization and asynchronous programming, allowing you to create expensive objects on demand, without blocking the main thread. Again, assuming you’re using async/await and tasks properly!


Here are some of the benefits of using async lazy initialization:


  1. Improved Performance: By deferring the creation of expensive objects until they’re needed, you can improve the startup performance of your application.


  2. Better Responsiveness: By running the initialization code asynchronously, you can ensure that your application remains responsive, even if the initialization takes a long time.


  3. Simplicity: The Lazy<Task<T>> pattern is simple and easy to use. It leverages the existing Lazy<T> and Task<T> classes in .NET, so there’s no need to learn a new API.


  4. Control: You can control where you want to pay the penalty for initialization costs. If it makes sense to declare variables at initialization time but not pay the penalty for them at that point, you now have a tool for that.


Async Lazy Considerations

However, like any tool, it should be used with purpose! Overuse of lazy initialization (whether synchronous or asynchronous) can lead to its own problems, such as increased memory usage and the potential for deadlocks if not handled correctly. Remember, if the problem you’re trying to solve is some operation taking a long time, Lazy<T> doesn’t actually inherently fix this, it just allows you to move it. If you can move it to an advantageous spot, great! But just moving it doesn’t necessarily address your issue.


When we mix in the async part of this, there’s another layer of complexity added, and complexity doesn’t mix very well with lazy initialization! Consider that if you need to be using Lazy<Task<T>> in the first place, you probably have some async/await code to call into or some other task that needs to run. If you need to be interfacing with other systems (i.e. reading files from disk, pulling data from databases, querying for results from a web API, etc…) then there is room for errors to occur. The more complexity and the more things you need to interface with when running this async code, the more room there is for errors.


You’re going to need to consider what it looks like if your Lazy<Task<T>> is running code that can fail. What does it even look like to have resiliency in something that is expected to run once to cache a result for you? I’m not here to prescribe a one-size-fits-all solution, but I do think you need to give this serious consideration. In this article, there’s an example provided that can allow some fault tolerance, but the author also states that this can look extremely different from scenario to scenario.


Stephen Toub’s AsyncLazy<T>

Stephen Toub, a partner software engineer from Microsoftproposed an AsyncLazy<T> class that combines the best of both worlds: the laziness of Lazy<T> and the asynchrony of Task<T>.


Here’s how it looks (code slightly modified from the original because the blog post is several years old):

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()).Unwrap())
    { }
}


In this code, AsyncLazy<T> is a subclass of Lazy<Task<T>>. It provides two constructors: one that takes a Func<T> and another that takes a Func<Task<T>>. Why do we still use Task.Run on the second constructor? Well, see this point directly from Stephen:


[If we didn’t wrap it in a Task.Run] it means that when a user accesses the Value property of this instance, the taskFactory delegate will be invoked synchronously.  That could be perfectly reasonable if the taskFactory delegate does very little work before returning the task instance.  If, however, the taskFactory delegate does any non-negligable work, a call to Value would block until the call to taskFactory completes.  To cover that case, the second approach is to run the taskFactory using [Task.Run], i.e. to run the delegate itself asynchronously, just as with the first constructor, even though this delegate already returns a Task<T>.  Of course, now [Task.Run] will be returning a Task<Task<T>>, so we use the Unwrap method in .NET 4 to convert the Task<Task<T>> into a Task<T>…


Stephen Toub


Here’s how you can use it:

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

// later...

MyClass result = await myObject.Value;


In the above code snippet, myObject is an AsyncLazy<MyClass> instance. The new MyClass() call will not be run until myObject.Value is accessed for the first time, and it will be run asynchronously.


Conclusion

Async lazy initialization is a powerful technique that can help you write more efficient and responsive applications in C#. By combining the Lazy<T> and Task<T> classes, you can defer the creation of expensive objects until they’re needed, and do so without blocking the main thread.


However, like any tool, it should be used judiciously. Overuse of lazy initialization (whether synchronous or asynchronous) can lead to its own problems, such as increased memory usage and the potential for deadlocks if not handled correctly. Additionally, as complexity adds up, considerations around fault tolerance and how you manage errors become a challenge.




Also published here.

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!