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.