The notion of SOP (Safe Object Publication) is that objects are made globally visible as whole after constructed or initialized to a given state. This assumption makes usage of immutable objects a lot simpler as you never need any read-side coordination.
For example:
class Foo {
public readonly int b;
public Foo (int b) {
this.b = b;
}
static Foo instance;
static void SetCurrentInstanceTo10 () {
instance = new Foo (10);
}
static int CurrentInstance () {
var f = Instance;
return f.b;
}
}
In the above example, under SOP callers of Foo.Instance expect that they will always read 10 from b.
Under the CLR's memory model, there's a release fence on each store of a byref.
On method SetCurrentInstanceTo10, the sequence of relevant memory accesses we'll see is:
//$0 is the temporary holding the freshly allocated instance of `Foo`
$0.b = 10
release fence
instance = $0
The release fence ensures that all stores previously to it are globally visible before any access happening after the fence.
On method CurrentInstance, this is the sequence of relevant memory accesses we'll see:
$0 = instance
implicit data-dependent fence
$1 = $0.b
The implicit fence ensures that all reads that depend on $0 will happen afterwards.
Assuming CurrentInstance see the new Foo object from SetCurrentInstanceTo10, the only allowed read outcome of CurrentInstance is 10.
Under mono's model, there's no SOP with the original code as there's no release fence before the store to instance.
It's possible for CurrentInstance to read either 0 or 10 from instance as the stores will happen out of order.
The fix for this case is to mark the instance field as volatile, which will insert acquire-release fences around access to it and
ensure SOP ordering of memory accesses.
The code in question is: https://github.com/dotnet/roslyn/blob/master/src/Dependencies/PooledObjects/ObjectPool%601.cs#L194
Given this two methods (with debug code removed):
private T _firstItem;
internal void Free(T obj)
{
if (_firstItem == null)
{
_firstItem = obj;
}
}
internal T Allocate()
{
T inst = _firstItem;
if (inst == null || inst != Interlocked.CompareExchange(ref _firstItem, null, inst))
{
inst = AllocateSlow();
}
return inst;
}
As you can see, under mono, a call to Free with a newly allocated or initialized object can then be seen from Allocate with the init bits out of order.