原子操作线程安全-我需要一个“镜像”原子读吗?

3
以下代码是否安全(考虑它在隔离状态下)以避免 torn read?
以下代码是否安全(考虑它在隔离状态下)以避免 torn read?
private static double flopsErrorMargin = 0.01d;

public static double FlopsErrorMargin {
    get {
        double result = flopsErrorMargin;
        Thread.MemoryBarrier();
        return result;
    }
    set {
        Interlocked.Exchange(ref flopsErrorMargin, value);
    }
}
Interlocked.Exchange()是原子写操作,因为在.NET平台上(除了64位环境的实现细节)不能保证double类型能够在单个操作中写入。
然而,在读取方面,我是否还需要一个“镜像”操作?例如,如果我不以原子方式读取值,仍然有获取损坏数据的风险吗?
我的直觉是不需要,因为我认为另一个内存访问(例如读取)不能与任何其他原子操作同时发生,即使该其他访问本身不是原子操作。但我想得到一些确认!

2
只是想提醒一下,就像许多其他非常低级的多线程问题一样,你最好编写不必处理这些类型问题的代码。在你甚至询问这样的问题之前,你应该确信使用较高级别的多线程框架并采用相当保守的同步方式所花费的时间是无法接受的慢。 - Servy
原则上来说这是不安全的,但实际上(至少对于x86架构),我从未能够重现分裂读取。请参见此答案以获取更多信息。我对此结果感到惊讶。当然,最好不要依赖于不被保证的行为。 - Dan Bryant
当然,值得注意的是,如果你的代码有0.00001%的失败几率,那么当它发生时,你实际上很难发现。只有在程序非常庞大和复杂的情况下,失败的几率才会足够高,以至于这样的错误诊断将会非常困难。@DanBryant - Servy
1
请注意,我在这里同意@Servy的观点;这真的不是进行同步的正确位置。您几乎总是希望在更高的级别上进行同步,以便这些问题甚至不会出现。 - Dan Bryant
针对关于为什么我在这里进行同步的评论:嗯,我通常不会尝试保持我的同步在如此细粒度的级别(当然,尝试这样做是愚蠢的!);但这是一个需要能够在任何时候更改的单个全局变量。没办法。 :) - Xenoprimate
显示剩余2条评论
2个回答

3
不,Torn读取是可能的。假设您的字段访问正在读取数据,并在Interlocked.Exchange中间部分交错,那么其他32位将是Exchange的更新值,因此会产生Torn读取。
要进行原子读取,您需要在32位机器上使用Interlocked.Read

在64位系统上,Read方法是不必要的,因为64位读操作已经是原子性的。在32位系统上,除非使用Read,否则64位读操作不是原子性的

这也意味着可能存在Torn值。
您可以按以下方式定义自己的double原子Read
public static double Read(ref double location)
{
    return Interlocked.CompareExchange(ref location, 0d, 0d);
}

这是 Interlocked.Read(long) 内部的实现方式。

1
Interlocked.Read 不支持 double,它只适用于 long (Int64) 变量。 - Dan Bryant
@DanBryant 虽然这是正确的,但关键点仍然相同。读取大于字长的内容不是原子操作。您需要同步。 - Sriram Sakthivel
1
@DanBryant 更新了帖子,展示了如何为 double 实现 Interlocked.Read - Sriram Sakthivel

2
你如果不原子地读取值,仍然有可能出现撕裂错误的风险吗?
是的。Interlocked.Exchange的返回值不会受到撕裂的影响,最终flopsErrorMargin的值将变为value(这是Interlocked.Exchange给予你的两个保证),但是未同步的读取访问可能会发生撕裂。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接