If I have a deeply immutable type (all members are readonly and if they are reference type members, then they also refer to objects that are deeply immutable).
I would like to implement a lazy initialized property on the type, like this:
private ReadOnlyCollection<SomeImmutableType> m_PropName = null;
public ReadOnlyCollection<SomeImmutableType> PropName
{
get
{
if(null == m_PropName)
{
ReadOnlyCollection<SomeImmutableType> temp = /* do lazy init */;
m_PropName = temp;
}
return m_PropName;
}
}
From what I can tell:
m_PropName = temp;
...is threadsafe. I'm not worried too much about two threads both racing to initialize at the same time, because it will be rare, both results would be identical from a logical perspective, and I'd rather not use a lock if I don't have to.
Will this work? What are the pros and cons?
Edit: Thanks for your answers. I will probably move forward with using a lock. However, I'm surprised nobody brought up the possibility of the compiler realizing that the temp variable is unnecessary, and just assigning straight to m_PropName. If that were the case, then a reading thread could possibly read an object that hasn't finished being constructed. Does the compiler prevent such a situation?
(Answers seem to indicate that the runtime won't allow this to happen.)
Edit: So I've decided to go with an Interlocked CompareExchange method inspired by this article by Joe Duffy.
Basically:
private ReadOnlyCollection<SomeImmutableType> m_PropName = null;
public ReadOnlyCollection<SomeImmutableType> PropName
{
get
{
if(null == m_PropName)
{
ReadOnlyCollection<SomeImmutableType> temp = /* do lazy init */;
System.Threading.Interlocked(ref m_PropName, temp, null);
}
return m_PropName;
}
}
This is supposed to ensure that all threads that call this method on this object instance will get a reference to the same object, so the == operator will work. It is possible to have wasted work, which is fine - it just makes this an optimistic algorithm.
As noted in some comments below, this depends on the .NET 2.0 memory model to work. Otherwise, m_PropName should be declared volatile.
m_PropName
fromnull
to non-null. Whichever thread's CAS gets to go first is the one that wins. This is a bit like how C and C++ compilers handlestatic
local variables with a non-static initializer, but they need a separate guard variable (because 0 / null is a valid value). Still, after the dust settles on init, the fast path through the function is an acquire load and test/branch on the guard variable. – Decidedm_propname
maybe doesn't need an acquire load since it is itself a reference. So dependency ordering makes it automatically safe to dereference, if that's true in C#. Likestd::memory_order_consume
in C++, which would be free in asm on all CPUs except Alpha if it compilers didn't just strengthen it toacquire
, so in practice you userelaxed
and depend on the compiler not to break the data dependency...) – Decided