When we have a long running operation, a common optimisation is to cache the result and reuse it in subsequent requests. Given the ongoing shift to multithreaded applications it is important that these caching mechanisms are (at least optionally) thread safe. That is, you don't want two concurrent calls to your cache to result in the long running operation executing twice.
Lazy<T> in the new System.Threading library is designed specifically for this purpose.
Func<string> f = () =>
{
Thread.Sleep(1000);
return "hello world";
};
var lazy = new Lazy<string>(f);
var sw = Stopwatch.StartNew();
Console.WriteLine("Is cached: " + lazy.IsValueCreated);
Console.WriteLine("Value: " + lazy.Value);
Console.WriteLine("Took: " + sw.Elapsed);
Console.WriteLine();
sw = Stopwatch.StartNew();
Console.WriteLine("Is cached: " + lazy.IsValueCreated);
Console.WriteLine("Value: " + lazy.Value);
Console.WriteLine("Took: " + sw.Elapsed);
OUTPUT
Is cached: False
Value: hello world
Took: 00:00:01.0100048
Is cached: True
Value: hello world
Took: 00:00:00.0003014 // second read uses the cached value
Of course function memoization can be achieved without Lazy<T>. Here is a a different implementation I've used in the past.
public static Func<T> Memoize<T>(this Func<T> f)
{
var gate = new object();
var set = false;
T result = default(T);
return () =>
{
if (!set)
{
lock (gate)
{
if (!set)
{
result = f();
set = true;
}
}
}
return result;
};
}
Giving a nice fluent usage.
Func<string> f = () =>
{
Thread.Sleep(1000);
return "hello world";
};
var memoized = f.Memoize();
var sw = Stopwatch.StartNew();
Console.WriteLine("Value: " + memoized());
Console.WriteLine("Took: " + sw.Elapsed);
Console.WriteLine();
sw = Stopwatch.StartNew();
Console.WriteLine("Value: " + memoized());
Console.WriteLine("Took: " + sw.Elapsed);
OUTPUT
Value: hello world
Took: 00:00:01.0018696
Value: hello world
Took: 00:00:00.0001825
So how do you choose between the two approaches?
Best thing about Lazy<T> is all the hard work has been done for you. Appropriate class name don't you think? Something it offers over a typical memoized function is the .IsValueCreated property that can be used to determine if the cache has been populated yet. On the other hand the simple .Memoize() implementation reads through approximately 30% quicker. I think they are both useful.
public static Func<T> Memoize<T>(this Func<T> f)
public static Lazy<T> AsLazy<T>(this Func<T> f)
More soon!
James
Comments