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 MemoryBarriers 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.