C#可以写指令从finally块重新排序到try块吗?

问题描述 投票:1回答:1

在一篇关于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_version1m_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),但问题是这是否真的需要?

示例中显示的MemoryBarriers是隐式的,由编写器锁的Interlocked方法生成,因此它们始终存在。危险在于读者可以在作家完成之前看到m_version2增加。

c# volatile memory-barriers
1个回答
1
投票

我没有发现任何会限制它的规范,所以我用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.Writenew Person()上存储volatileperson关键字。 所以,如果你有一些数据竞赛,你将面对它们。

© www.soinside.com 2019 - 2024. All rights reserved.