在一篇关于A scalable reader/writer scheme with optimistic retry的文章中有一个代码示例:
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;
}
}
如果我使m_version1
和m_version2
不易变,但然后使用代码:
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
}
}
来自m_version2 = m_version2 + 1
线的指令可以从finally
重新排序到try
区块吗?在m_version2
增加之前,作家完成是很重要的。
逻辑上finally
是在try
之后执行的,但是在finally
中没有提到list of implicit memory barriers块。如果finally
的指令可以在try
的指令之前移动,那将是非常令人困惑的,但是在指令级别的CPU优化对我来说仍然是一个黑魔法。
我可以把Thread.MemoryBarrier();
放在m_version2 = m_version2 + 1
线之前(或使用Volatile.Write
),但问题是这是否真的需要?
示例中显示的MemoryBarrier
s是隐式的,由编写器锁的Interlocked
方法生成,因此它们始终存在。危险在于读者可以在作家完成之前看到m_version2
增加。
我没有发现任何会限制它的规范,所以我用ARM CPU设备检查它(使用Xamarin,必须在Core CLR上检查)... 一个线程正在执行此代码:
try
{
person = new Person();
}
finally
{
isFinallyExecuted = true;
}
而第二个线程正在等待isFinallyExecuted
成为true
与此代码:
while (!Volatile.Read(ref isFinallyExecuted))
;
然后第二个线程正在执行以下代码:
if (!person.IsInitialized())
{
failCount++;
Log.Error("m08pvv", $"Reordered from finally: {failCount}, ok: {okCount}");
}
else
{
okCount++;
}
IsInitialized
方法检查所有字段是否已正确设置,因此它返回部分构造对象的false
。
这是我在日志中得到的:
12-25 17:00:55.294:E / m08pvv(11592):从最终重新排序:48,确定:682245 12-25 17:00:56.750:E / m08pvv(11592):从最后重新排序:49,确定:686534 12-25 17:00:56.830:E / m08pvv(11592):从最终重新排序:50,确定:686821 12-25 17:00:57.310:E / m08pvv(11592):从最后重新排序:51,确定:688002 12-25 17:01:12.191:E / m08pvv(11592):从最后重新排序:52,好的:733724 12-25 17:01:12.708:E / m08pvv(11592):从最后重新排序:53,确定:735338 12-25 17:01:13.722:E / m08pvv(11592):从最后重新排序:54,好的:738839 12-25 17:01:25.240:E / m08pvv(11592):从最后重新排序:55,确定:775645
这意味着对于775645成功运行该代码,55次我得到isFinallyExecuted
等于true
和部分构造的对象。这是可能的,因为我不使用Volatile.Write
在new Person()
上存储volatile
或person
关键字。
所以,如果你有一些数据竞赛,你将面对它们。