In an article about A scalable reader/writer scheme with optimistic retry there is a code example:
using System;
using System.Threading;
public class OptimisticSynchronizer
{
private volatile int m_version1;
private volatile int m_version2;
public void BeforeWrite() {
++m_version1;
}
public void AfterWrite() {
++m_version2;
}
public ReadMark GetReadMark() {
return new ReadMark(this, m_version2);
}
public struct ReadMark
{
private OptimisticSynchronizer m_sync;
private int m_version;
internal ReadMark(OptimisticSynchronizer sync, int version) {
m_sync = sync;
m_version = version;
}
public bool IsValid {
get { return m_sync.m_version1 == m_version; }
}
}
public void DoWrite(Action writer) {
BeforeWrite();
try {
writer(); // this is inlined, method call just for example
} finally {
AfterWrite();
}
}
public T DoRead<T>(Func<T> reader) {
T value = default(T);
SpinWait sw = new SpinWait();
while (true) {
ReadMark mark = GetReadMark();
value = reader();
if (mark.IsValid) {
break;
}
sw.SpinOnce();
}
return value;
}
}
If I make m_version1
and m_version2
not volatile but then use the code:
public void DoWrite(Action writer) {
Thread.MemoryBarrier(); // always there, acquiring write lock with Interlocked method
Volatile.Write(ref m_version1, m_version1 + 1); // NB we are inside a writer lock, atomic increment is not needed
try {
writer();
} finally {
// is a barrier needed here to avoid the increment reordered with writer instructions?
// Volatile.Write(ref m_version2, m_version2 + 1); // is this needed instead of the next line?
m_version2 = m_version2 + 1; // NB we are inside a writer lock, atomic increment is not needed
Thread.MemoryBarrier(); // always there, releasing write lock with Interlocked method
}
}
Could instructions from line m_version2 = m_version2 + 1
be reordered from finally
into try
block? It is important that a writer finishes before m_version2
is incremented.
Logically finally
is executed after try
, but the finally
block is not mentioned in the list of implicit memory barriers. It would be quite confusing if instructions from finally
could be moved before the ones from try
, but CPU optimizations at instructions level are still a black magic for me.
I could put Thread.MemoryBarrier();
before the line m_version2 = m_version2 + 1
(or use Volatile.Write
), but the question is if this is really needed?
The MemoryBarrier
s shown in the example are implicit and generated by Interlocked
methods of a writer lock, so they are always there. The danger is that a reader could see m_version2
incremented before the writer finishes.