3

Ok, so I've been looking at the source code for Lazy<T> because I want to extend it. I know that in theory, it is supposed to be thread safe, but I don't know how it can be. Looking it's it .value getter, it does not lock on anything before reading the value.

public T Value
{
    get
    {
        Boxed boxed = null;
        if (m_boxed != null )
        {
            // Do a quick check up front for the fast path.
            boxed = m_boxed as Boxed;
            if (boxed != null)
            {
                return boxed.m_value;
            }
              LazyInternalExceptionHolder exc = m_boxed as LazyInternalExceptionHolder;
            Contract.Assert(m_boxed != null);
            exc.m_edi.Throw();
        }

        // Fall through to the slow path.
#if !FEATURE_CORECLR
        // We call NOCTD to abort attempts by the debugger to funceval this property (e.g. on mouseover)
        //   (the debugger proxy is the correct way to look at state/value of this object)
        Debugger.NotifyOfCrossThreadDependency(); 
#endif
        return LazyInitValue();

    }
}

It's my understanding that to be thread safe, something must lock when both writing as well as reading, because if a read get's interrupted by a write it can return incorrect data or even have an error. Is this understanding correct, or is something complicated happening with Lazy<T>?

Sidney
  • 624
  • 7
  • 20
  • 1
    The code you're looking at is just a variation on the double-checked lock pattern, which is safe as described in the marked duplicate. For additional information, you may want to read other Q&A such as https://stackoverflow.com/questions/1964731/the-need-for-volatile-modifier-in-double-checked-locking-in-net, https://stackoverflow.com/questions/6334731/again-double-checked-locking-and-c-sharp, and https://stackoverflow.com/questions/6092748/why-double-checked-locking-is-used-at-all – Peter Duniho Jul 11 '16 at 22:23
  • 1
    The "something complicated" is in LazyInitValue. That is where the magic that ensures correctness in the face of multiple threads happens. – Eric Lippert Jul 11 '16 at 22:25
  • 2
    Your statement that thread safety *requires* locking on reading and writing is incorrect. This code is an example of a lock elision pattern written by experts who know *precisely* what sequence of memory barriers, reads and writes is necessary to ensure both correctness and high performance, and it is narrowly tuned to the specific scenario of compute-once-and-publish-to-a-property. Do not attempt to write code like this yourself! If you're trying to make something threadsafe, consistently take out locks. – Eric Lippert Jul 11 '16 at 22:33
  • @EricLippert, a colleague of mine is recommending that Volatile.Read is required when accessing a Lazy that is allocated on one thread and on another. Your thoughts? Here's some code: public class { private Lazy _prop1; public void Init() { _prop1 = InterlockedExchange(ref _prop1, new Lazy(() => new SomeType()); } public SomeType GetSomeType() { return _prop1.Value; } } – Simon Gillbee Oct 05 '16 at 13:57
  • @EricLippert, from previous comment... In this code, it is possible that Init() is called multiple times on multiple threads (think in response to RX trigger) and GetSomeType is accessed on yet another thread. My colleague is suggesting that even though the Lazy is itself threadsafe, the atomic assignment to _prop1 is vulnerable to memory barrier problems and so a different core could access an older value. Thus a Volatile.Read is required. – Simon Gillbee Oct 05 '16 at 14:08
  • 1
    @SimonGillbee: Does it not strike you as somewhat odd to *lazily initialize* a field of type Lazy? Why not just eagerly initialize it? – Eric Lippert Oct 05 '16 at 15:41

1 Answers1

7

Read/writes to a variable of a reference type are atomic, so it's not possible for such a read to ever return a value that was not written to it, even with no locking. The value being read there is only ever assigned once, when the Lazy generates its value, so either the value is null, and it moves on to the more complex logic, or it isn't, and we already have a value to return. If it moves on it does actually use locking mechanisms to ensure multiple threads aren't attempting to create the value at the same time.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • Am I correct in thinking this is double-checked locking? – Lukazoid Jul 11 '16 at 22:21
  • 2
    @Lukazoid: It is a variation on double-checked locking. If you read the code you'll see that m_boxed is checked for nullity once outside the lock and once inside, so there is a double check. – Eric Lippert Jul 11 '16 at 22:35