10
\$\begingroup\$

I implemented a resettable version of .NET Lazy (context: mostly to use it in a MemoryCache for auto refresh purposes on item expiration)

This is a very naive implementation and I am pretty sure there are couple of things that are not okay. In particular, I am not really sure whether the reset is fine. I think the assignment is atomic but I am not sure what can happen when there is an attempt to get the Value while it is resetting at the same (e.g. I am not sure whether the fact that the assignment is atomic is enough to guarantee proper operations).

public class ResettableLazy<T>
{
    private Lazy<T> _lazy;

    public bool IsValueCreated => _lazy.IsValueCreated;

    public T Value => _lazy.Value;

    public LazyThreadSafetyMode LazyThreadSafetyMode { get; }

    private readonly Func<T> _valueFactory;

    public ResettableLazy(Func<T> valueFactory, LazyThreadSafetyMode lazyThreadSafetyMode = LazyThreadSafetyMode.ExecutionAndPublication)
    {
        _valueFactory = valueFactory;
        LazyThreadSafetyMode = lazyThreadSafetyMode;
        _lazy = new Lazy<T>(_valueFactory, LazyThreadSafetyMode);
    }

    public ResettableLazy(Func<T> valueFactory, bool isThreadSafe)
        : this(valueFactory, isThreadSafe ? LazyThreadSafetyMode.ExecutionAndPublication : LazyThreadSafetyMode.None)
    {
    }

    public void Reset()
    {
        _lazy = new Lazy<T>(_valueFactory, LazyThreadSafetyMode);
    }
}

And played a bit around with:

public static class Program
{
    public static void Main(params string[] args)
    {
        var lazy = new ResettableLazy<int>(() =>
        {
            Thread.Sleep(2000);
            return new Random().Next();
        });

        for (var i = 0; i < 1000; i++)
        {
            if (i % 2 == 0)
            {
                Thread.Sleep(200);
            }

            Task.Run(() => Console.WriteLine(lazy.Value));

            if (i % 3 == 0)
            {
                Task.Run(() => lazy.Reset());
            }
        }

        Console.ReadKey();
    }
}

Is there any red flags in my implementation?

\$\endgroup\$
  • 3
    \$\begingroup\$ It might be easier to cache the object that contains the Lazy<T>. When it expires you create a new one. IOW, the exact opposite of what you're doing. Instead of resetting Lazy so that it reads from the cache, cache the Lazy so that it expires. \$\endgroup\$ – Scott Hannen Dec 18 '17 at 20:47
  • 1
    \$\begingroup\$ @ScottHannen or even better: create an interface and add a decorator like ExpirableLazy<T> taking a ResettableLazy<T> as a parameter. \$\endgroup\$ – t3chb0t Dec 21 '17 at 20:41
  • 1
    \$\begingroup\$ @t3chb0t IExpirer<TLazy> where TLazy : ResettableLazy<ExpirableLazy<T>> \$\endgroup\$ – Scott Hannen Dec 21 '17 at 22:03
  • \$\begingroup\$ @ScottHannen Actually I got various implementations to achieve my goal I thought the lazy fashion was interesting to explore. But yeah this is pretty much what I ended up doing. \$\endgroup\$ – Ehouarn Perret Dec 22 '17 at 13:41
2
\$\begingroup\$

Overall, it looks good to me. There aren't any real threading concerns with simultaneously resetting and accessing the value. The body of the Value property getter will either use the old _lazy or the new _lazy, but those are the only options. Since there isn't any locking it is possible for one thread to continue to see the old value after another thread resets (due to caching a value of the _lazy reference), but this is likely not a problem in most use cases.

One thing to note is that if Lazy<T>.IsValueCreated ever becomes true it stays true for the lifetime of the object, and this is not the case with your class. It's worth calling out that if the object is shared between threads than many of the reasons to use IsValueCreated become less reliable (e.g. accessing .Value may be expensive).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.