1

Here is a nice class that implements a subset of .Net 4.0's System.Lazy. It's been working fine for quite some time, but today something odd happened. The InvalidOperationException, below, was thrown in a case where Lazy.Value.get was nowhere to be found in the call stack (or in any other thread for that matter) except for the frame that was throwing the exception.

public class Lazy<T>
{
    readonly object _locker = new object();
    Func<T> _func;
    T _value;
    bool _busy;
    public Lazy(Func<T> func)
    {
        if (ReferenceEquals(func, null))
            throw new ArgumentNullException("func");
        _func = func;
    }
    public bool IsValueCreated { get { return _func == null; } }
    public T Value
    {
        get
        {
            if (_func != null)
                lock (_locker)
                    if (_busy)
                        throw new InvalidOperationException("Function evaluation recursed back into Value.get on the same thread.");
                    else if (_func != null)
                    {
                        _busy = true;
                        try
                        {
                            _value = _func();
                            _func = null;
                        }
                        finally { _busy = false; }
                    }
            return _value;
        }
    }
}

How could this happen? Is there any way the finally{} block could have been skipped? A breakpoint at _busy=true was never hit; is there any way _busy could have defaulted to true?

This is happening in the midst of an Office 2007 integration using Addin Express, could there be some unsafe libraries messing with Lazy's bits, or maybe a .Net framework version compatibility issue (this was built targetting 3.5)?

Yannick Blondeau
  • 9,465
  • 8
  • 52
  • 74
Joshua Tacoma
  • 408
  • 5
  • 13
  • 7
    Personally I'd have more confidence in your code if you included braces everywhere - it's *very* easy to get taken in by whitespace implying structure incorrectly. Also, you're using double-checked locking without making `_func` volatile. Bad idea. – Jon Skeet Oct 01 '12 at 18:33
  • Thanks, I'll make those changes and post back in case making _func volatile prevents this from happening again. – Joshua Tacoma Oct 01 '12 at 18:38
  • 1
    @JonSkeet: Can you elaborate on the point about making _func volatile when using double-checked locking? – Eric J. Oct 01 '12 at 18:38
  • 1
    @EricJ.: Read http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html - while .NET's memory model isn't exactly the same, it's close enough that there are problems unless you have a memory barrier. Personally I'd avoid even *trying* to do this sort of thing myself. Have you validated that a simpler implementation (which *always* acquires the lock) is actually too slow? – Jon Skeet Oct 01 '12 at 18:39
  • The only way I can see the problem happening is if `func()` accessed the same Lazy(T) that just called it, but that should show up in the stack trace. The double checked locking is indeed something I'd not play with lightly though, it may very well be the cause too. – Joachim Isaksson Oct 01 '12 at 18:42
  • @JoshuaTacoma: Look at this question that was also just asked. May provide a simple solution to this very problem http://stackoverflow.com/a/12678351/141172 – Eric J. Oct 01 '12 at 18:43
  • 1
    The consistency with which this situation occurred has ended as mysteriously as it began. I've decided to implement the "always lock" suggestion from Jon Skeet. If I ever manage to determine that this solves the issue (as Joachim Isaksson suggests) I'll post the revised code as an answer here. – Joshua Tacoma Oct 01 '12 at 19:11
  • 1
    @JoshuaTacoma The thing is that I can't obviously see how `_busy` in any way could get the wrong value from the double checked lock, but once you're on the slippery slope of possible memory barrier race conditions, it gets _very_ tricky to rule any side effect out entirely, especially since the `func()` call is not known. – Joachim Isaksson Oct 01 '12 at 19:38

0 Answers0