C# 可以将写入指令从最终块重新排序为 try 块吗?



在一篇关于具有乐观重试的可扩展读取器/写入器方案的文章中,有一个代码示例:

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非常重要。

逻辑上finallytry之后执行,但finally块在隐式内存屏障列表中没有提及。如果来自finally的指令可以在来自try的指令之前移动,那将是相当令人困惑的,但是指令级别的CPU优化对我来说仍然是一个黑魔法。

我可以Thread.MemoryBarrier();放在行m_version2 = m_version2 + 1之前(或使用Volatile.Write),但问题是这是否真的需要?

示例中所示的MemoryBarrier是隐式的,由编写器锁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, ok: 738839
12-25 17:01:25.240: E/m08pvv(11592): 重新排序 最后: 55, 确定: 775645

这意味着775645成功运行该代码,我得到的 55 次isFinallyExecuted等于true部分构造的对象。这是可能的,因为我不使用Volatile.Write来存储new Person()或在personvolatile关键字。

所以,如果你有一些数据竞赛,你将面对它们。

最新更新