为什么此代码不产生撕裂读取?

Why doesn't this code produce torn reads?

在查看 CLR/CLI 规格和内存模型等时,我注意到根据 ECMA CLI spec:

围绕原子 reads/writes 的措辞

A conforming CLI shall guarantee that read and write access to properly aligned memory locations no larger than the native word size (the size of type native int) is atomic when all the write accesses to a location are the same size.

特别是短语“正确对齐内存”引起了我的注意。我想知道我是否可以通过一些技巧在 64 位系统上使用 long 类型进行撕裂读取。所以我写了下面的测试用例:

unsafe class Program {
    const int NUM_ITERATIONS = 200000000;
    const long STARTING_VALUE = 0x100000000L + 123L;
    const int NUM_LONGS = 200;
    private static int prevLongWriteIndex = 0;

    private static long* misalignedLongPtr = (long*) GetMisalignedHeapLongs(NUM_LONGS);

    public static long SharedState {
        get {
            Thread.MemoryBarrier();
            return misalignedLongPtr[prevLongWriteIndex % NUM_LONGS];
        }
        set {
            var myIndex = Interlocked.Increment(ref prevLongWriteIndex) % NUM_LONGS;
            misalignedLongPtr[myIndex] = value;
        }
    }

    static unsafe void Main(string[] args) {
        Thread writerThread = new Thread(WriterThreadEntry);
        Thread readerThread = new Thread(ReaderThreadEntry);

        writerThread.Start();
        readerThread.Start();

        writerThread.Join();
        readerThread.Join();

        Console.WriteLine("Done");
        Console.ReadKey();
    }

    private static IntPtr GetMisalignedHeapLongs(int count) {
        const int ALIGNMENT = 7;
        IntPtr reservedMemory = Marshal.AllocHGlobal(new IntPtr(sizeof(long) * count + ALIGNMENT - 1));
        long allocationOffset = (long) reservedMemory % ALIGNMENT;
        if (allocationOffset == 0L) return reservedMemory;
        return reservedMemory + (int) (ALIGNMENT - allocationOffset);
    }

    private static void WriterThreadEntry() {
        for (int i = 0; i < NUM_ITERATIONS; ++i) {
            SharedState = STARTING_VALUE + i;
        }
    }

    private static void ReaderThreadEntry() {
        for (int i = 0; i < NUM_ITERATIONS; ++i) {
            var sharedStateLocal = SharedState;
            if (sharedStateLocal < STARTING_VALUE) Console.WriteLine("Torn read detected: " + sharedStateLocal);
        }
    }
}

然而,无论我运行 程序多少次,我都没有合法地看到行"Torn read detected!"。那为什么不呢?

我在一个块中分配了多个 longs,希望至少其中一个会在两个缓存行之间溢出;并且第一个 long 的 'start point' 应该错位(除非我误会了什么)。

我也知道多线程错误的性质意味着它们很难强制执行,而且我的 'test program' 没有达到应有的程度,但我 运行现在编程将近 30 次但没有结果 - 每次迭代 200000000 次。

这个程序有很多隐藏撕裂读取的缺陷。推理非同步线程的行为从来都不是简单的,也很难解释,意外同步的可能性总是很高。

  var myIndex = Interlocked.Increment(ref prevLongWriteIndex) % NUM_LONGS;

Interlocked 没有什么特别微妙的地方,不幸的是它对 reader 线程也有很大影响。很难看到,但您可以使用 Stopwatch 为线程的执行计时。您会看到 Interlocked on the writer 将 reader 减慢了 ~2 倍。够影响时序了reader又重现不了问题,意外同步。

消除危险并最大程度地提高检测到读取撕裂的几率的最简单方法是始终从同一内存位置读取和写入。修复:

  var myIndex = 0;

  if (sharedStateLocal < STARTING_VALUE)

此测试对检测撕裂读取没有多大帮助,有很多根本不会触发测试。 STARTING_VALUE 中有太多二进制零使它变得更加不可能。最大化检测几率的一个很好的替代方法是在 1 和 -1 之间交替,确保字节值始终不同并使测试非常简单。因此:

private static void WriterThreadEntry() {
    for (int i = 0; i < NUM_ITERATIONS; ++i) {
        SharedState = 1;
        SharedState = -1;
    }
}

private static void ReaderThreadEntry() {
    for (int i = 0; i < NUM_ITERATIONS; ++i) {
        var sharedStateLocal = SharedState;
        if (Math.Abs(sharedStateLocal) != 1) {
            Console.WriteLine("Torn read detected: " + sharedStateLocal);
        }
    }
}

这很快就会让您在 32 位模式下的控制台中阅读几页残缺不全的内容。要让它们也支持 64 位,您需要做额外的工作来获取变量 mis-aligned。它需要跨越 L1 cache-line 边界,因此处理器必须执行两次读取和写入,就像在 32 位模式下一样。修复:

private static IntPtr GetMisalignedHeapLongs(int count) {
    const int ALIGNMENT = -1;
    IntPtr reservedMemory = Marshal.AllocHGlobal(new IntPtr(sizeof(long) * count + 64 + 15));
    long cachelineStart = 64 * (((long)reservedMemory + 63) / 64);
    long misalignedAddr = cachelineStart + ALIGNMENT;
    if (misalignedAddr < (long)reservedMemory) misalignedAddr += 64;
    return new IntPtr(misalignedAddr);
}

-1 和 -7 之间的任何 ALIGNMENT 值现在也会在 64 位模式下产生撕裂读取。