All about the Lazy<T> Data Structure in C# .Net

.Net Lazy<T> is a type which describes one of many ways the framework deals and stores data structures. Confused? Let’s define and differentiate these two.

A data structure is an abstract description of a way of organizing / structuring data to allow specific operations on it to be performed efficiently, data structures are always made up of types and or other structures.

A type on the other hand is just concrete representation of an object, its’ properties and operations/methods. In order to understand the Lazy data structure we have to familiarize ourselves with its benefits and limitations.

Lazy<T> benefits:

So, what does using Lazy provide us with? Why should we use it?

Lazy<T> provides us with a clean out of the box way to differ loading an object until a time which it’s needed. This could be extremely beneficial if the object’s operations involves creating or utilizing expensive resources (high CPU usage, I.O operations or Ram allocation etc.)

Consider this Student class example:

public  class Student
 {
     public int Id { get; set; }
     public Student()
     {
         //Thread.Sleep(5000);
     }
     public Student(int id)
     {
         Id = id;
     }
 }

This is just a simple student class we will use to demonstrate some Laziness (Good laziness of course).

Talk is cheap, let create some unit tests. I will use Xunit to create a few unit tests to test the following:

  1. Test the Laziness of a Lazy<T>
  2. Test Lazy<T> thread safety

First, we test the laziness of this type, the following test will pass because the IsValueCreated returns false before we assign Value property of the Lazy<T>.

[Fact]
public void Should_CreateLazyStudent_Using_Default_Constructor_And_Return_False()
{
   var lazyStudent = new Lazy<Student>();
   var  loaded = lazyStudent.IsValueCreated;
   Assert.False(loaded);
}

In the next test, we will prove that the Lazy<T> doesn't get created before it Value is used or assigned to a variable.

 [Fact]
public void Should_CreateLazyStudent_Using_Default_Constructor_And_Return_True()
{
    var lazyStudent = new Lazy<Student>();
    var value = lazyStudent.Value;
   var loaded = lazyStudent.IsValueCreated;
    Assert.True(loaded);
}

Both tests clearly demonstrate the true Laziness of the Lazy<T> type. You can further prove that by placing a debug breakpoint in the Student class constructor during the tests and run the test in debug mode (constructor will only be called using the second test when we attempt to use the Value property).

Lazy<T> Thread safety:

It's critical to understand that Lazy<T> is default thread safe, this doesn't mean your T properties and methods also thread safe.Ensuring thread safety of all properties and methods are the developer's responsibility.

Another way to specify thread safety and how a Lazy instance synchronizes access among multiple threads is by using LazyThreadSafetyMode enumeration.

In our next three unit tests, we will use all of these enumerations.

Since we are testing thread safety, you need to uncomment the Thread.Sleep(5000); line in the Student class constructor before running the thread safety tests.

The first one to use is LazyThreadSafetyMode.None.Using None basically means we don't want any thread safety, this should be OK to use if are sure that only one thread can access the value of T.

    [Fact]
    public void Should_Create_ThreadSafe_LazyStudent_Using_LazyThreadSafetyMode_None()
    {
        var lazyStudent = new Lazy<Student>(LazyThreadSafetyMode.None);
        //alternatively, you can use false instead of LazyThreadSafetyMode.None
        //var lazyStudent = new Lazy<Student>(false);
        var students = new ConcurrentBag<Task<Student>>();
      Task.Run(  () =>
        {
            Parallel.For(0, 2, i =>
            {
                students.Add(Task.Run(() => lazyStudent.Value));
            });
        }).Wait();
            var areEqual =  object.ReferenceEquals(students.First().Result, students.Last().Result);
            Assert.True(areEqual);
    }

Because we set ThreadSatetyMode to None, the above test fails as the code will try to create two students in a multi-threaded manner without any thread safety.

Next unit test will test LazyThreadSafetyMode.ExecutionAndPublication.

LazyThreadSafetyMode.ExecutionAndPublication guarantees only one instance of T is created, the same instance will always be returned to the caller.

[Fact]
public void Should_Create_ThreadSafe_LazyStudent_Using_LazyThreadSafetyMode_ExecutionAndPublication()
    {
        var lazyStudent = new Lazy<Student>(LazyThreadSafetyMode.ExecutionAndPublication);
        var students = new ConcurrentBag<Task<Student>>();
        Task.Run(() =>
        {
            Parallel.For(0, 2, i =>
            {
                students.Add(Task.Run(() => lazyStudent.Value));
            });
        }).Wait();
        var areEqual = object.ReferenceEquals(students.First().Result, students.Last().Result);
        Assert.True(areEqual);
    }

If you place a debug breakpoint in the Student constructor you will see that it will only be called once.

The last enumeration to test is LazyThreadSafetyMode.PublicationOnly. Unlike ExecutionAndPublication when multiple threads try to initialize a Lazy<T> instance simultaneously using PublicationOnly, all threads are allowed to run the initialization method or constructor, However, the first thread to complete the initialization wins and it's instance is returned to all callers.If you place a debug breakpoint on the Student constructor, you will see that its called multiple times, However, we can prove that the same instance is returned to caller (our test method) by comparing both instance using object.ReferenceEquals and the comparison always return true.

[Fact]
public void Should_Create_ThreadSafe_LazyStudent_Using_LazyThreadSafetyMode_PublicationOnly()
{
    var student = new Lazy<Student>( LazyThreadSafetyMode.PublicationOnly);
    var concurrentBag = new ConcurrentBag<Task<Student>>();
    Task.Run(() =>
    {
        Parallel.For(0, 2, loop => concurrentBag.Add(Task.Run(() => student.Value)));
    }).Wait();
    var areEqual = object.ReferenceEquals(concurrentBag.First().Result, concurrentBag.Last().Result);
    Assert.True(areEqual);
}

Finally, there is an alternative way to create a Lazy<T>, you can use a Func<out TResult>. like so:

Func<Student> initStudent = () =>
{
    return new Student();
};
[Fact]
public void Should_Create_ThreadSafe_LazyStudent_Using_Using_Initialization_Method()
{
    var lazyStudent = new Lazy<Student>(() => initStudent(), LazyThreadSafetyMode.None);
    var students = new ConcurrentBag<Task<Student>>();
    Task.Run(() =>
    {
        Parallel.For(0, 2, i =>
        {
            students.Add(Task.Run(() => lazyStudent.Value));
        });
    }).Wait();
    var areEqual = object.ReferenceEquals(students.First().Result, students.Last().Result);
    Assert.True(areEqual);
}

If you run this unit test in debug mode, you will see that it throws an exception with the following message "ValueFactory attempted to access the Value property of this instance". The reason for the exception is obvious, we are using LazyThreadSafetyMode.None and we have a race condition resulting from the two threads created in Parallel loop. Using as initialization method causes the Lazy<T> to cache exceptions (see final thoughts).

Final thoughts

Microsoft Lazy<T> documentation states that In addition to specifying the thread safety of a Lazy<T> instance, the LazyThreadSafetyMode enumeration affects exception caching. When exceptions are cached for a Lazy instance, you get only one chance to initialize the instance.If an exception is thrown the first time you call the Lazy<T>.Value property, that exception is cached and rethrown on all subsequent calls to the Lazy<T>.Value property. The advantage of caching exceptions is that any two threads always get the same result, even when errors occur.When you specify the PublicationOnly mode, exceptions are never cached. When you specify None or ExecutionAndPublication, caching depends on whether you specify an initialization method or allow the default constructor for T to be used. Specifying an initialization method enables exception caching for these two modes. If you use a constructor that does not specify an initialization method, exceptions that are thrown by the default constructor for T are not cached.

All duplicate code was removed from the sample application Download, unzip the file and start your own experiments.

Any thoughts? hit the comment box.

Add comment