Nuget package to implement the Lazy Initialization pattern in a thread-safe and efficient manner.
public class SampleService
{
private readonly Func<ExpensiveObject> _expensiveLoad = LazyProperty.Create(() => new ExpensiveObject());
public ExpensiveObject ExpensiveLoad => _expensiveLoad();
public void DoWork(int n) => ExpensiveLoad.Move(n*10 - 100, n*10 + 100);
}
SampleService
depends on ExpensiveObject
to do some work.
The creation of a ExpensiveObject
is a computational intensive task.
The SampleService
's ExpensiveLoad
property will return a new ExpensiveObject
when read the first time. Moreover, It will cache this result in a thread-safe and efficient manner.
Follow the steps from the LazyPropertyHelper
Nuget Package.
None
Yes. Sample project.
Yes.
Developers that have written code similar to this one:
public class MyServiceNaive
{
private ExpensiveObject expensiveLoad;
public ExpensiveObject ExpensiveLoad
{
get
{
if (expensiveLoad == null)
{
expensiveLoad = new ExpensiveObject();
}
return expensiveLoad;
}
}
//more code
}
The ExpensiveLoad
property is not thread-safe. It can be subject to weird race conditions.
The following code is thread-safe. However, it is inefficient because it acquires a lock whenever ExpensiveLoad
is read.
The LazyPropertyHelper
nuget prevents unnecessary locks.
public class MyLockedService
{
private object criticalSection = new object();
private ExpensiveObject expensiveLoad;
public ExpensiveObject ExpensiveLoad
{
get
{
lock (criticalSection)
{
if (expensiveLoad == null)
{
expensiveLoad = new ExpensiveObject();
}
}
return expensiveLoad;
}
}
}
LazyPropertyHelper
leverages the advantages of lambdas and functional programming to cache the result of an expensive computation. The computation is executed only once in a thread-safe context. Subsequent reads don't require a lock.
Here's the code where all of this takes place. The important piece is the CalculateAndCacheExpensiveComputation
method that replaces the _expensiveComputationReader
with a lambda that always return the cached value from the _cachedResult
field.
Testing concurrent code is hard [citation needed]. We didn't even bother to simulate concurrency problems in unit tests.
However, we wrote dozens of unit tests to make sure that the LazyPropertyHelper
behaves as expected.
I did not create this pattern. I was inspired by a similar implementation from this great book about functional programming in Java.
Isn't this the same as Lazy<T>
?
Lazy<T>
solves many of the problems around lazy initialization. You could even argue how much richer it is.
However, Lazy<T>
adopts an all or nothing approach to locking. It can be configured to never lock or to always lock. These extremes can be highly inefficient or dangerous in certain situations.
LazyPropertyHelper
takes a simpler approach with no compromises on thread-safety and performance.
No. There are several unit tests to cover the destruction of the cached objects.
The result of the expensive computation can be explicitly disposed from the Dispose
method of the class with the lazy property.