【问题标题】:Reading an int that's updated by Interlocked on other threads读取由 Interlocked 在其他线程上更新的 int
【发布时间】:2014-09-08 14:31:32
【问题描述】:

(这是重复的:How to correctly read an Interlocked.Increment'ed int field? 但是,在阅读了答案和 cmets 之后,我仍然不确定正确的答案。)

有些代码我不拥有并且无法更改为使用在几个不同线程中递增 int 计数器 (numberOfUpdates) 的锁。所有调用使用:

Interlocked.Increment(ref numberOfUpdates);

我想在我的代码中读取 numberOfUpdates。现在,由于这是一个 int,我知道它不能撕裂。但是,确保我获得最新价值的最佳方式是什么?看来我的选择是:

int localNumberOfUpdates = Interlocked.CompareExchange(ref numberOfUpdates, 0, 0);

或者

int localNumberOfUpdates = Thread.VolatileRead(numberOfUpdates);

两者都可以工作吗(无论优化、重新排序、缓存等如何,都可以提供最新的价值)?一个比另一个更受欢迎吗?还有第三种更好的选择吗?

【问题讨论】:

  • 这似乎是一个常见问题,请参阅stackoverflow.com/questions/516863/…。但我仍然不清楚 Thread.VolatileRead 或 Interlocked.CompareExchange 是否更好或无关紧要。
  • 一个值得考虑的问题:接下来会发生什么依赖于获取 numberOfUpdates 的“最新”值,以及它如何与任何增加它的东西相互作用?如果您读取稍微陈旧的缓存值会有什么后果?
  • 没什么。如果我得到一个旧的值,那会很好,只要我最终看到正确的值。我不太担心读取 int 并获得 100 毫秒前的值。我更关心一些被优化或缓存在寄存器等中的东西,以便我的读取 never 看到更新的值。我不明白这怎么会发生。但我只是认为我应该以最好的、最惯用的 C# 方式阅读 int。而且我不确定 Interlocked.CompareExchange 或 Thread.VolatileRead 是否更好,或者是否没有区别。
  • 最好的方法是一开始就不要这样做。多线程是个坏主意。如果你必须有多个线程,共享内存是个坏主意。如果你必须共享内存,锁规避是一个坏主意。 对共享内存的每次访问都加锁。如果结果证明这是一个不可接受的性能负担,并且您无法消除争用,或者即使没有争用也存在负担,那么只有那么您才应该考虑评估低锁定解决方案。
  • 我不需要性能,所以没有低锁代码的目标。其他开发人员已经在使用任何锁之外的 Interlocked.* 来修改这个整数,所以锁不是我的选择。我只需要以最好的方式阅读这个 int ,因为它是在锁之外更新的(尽管据我所知总是使用 Interlocked.* )。如果我从头开始,我会锁定所有地方以使其更简单。

标签: c# .net multithreading interlocked interlocked-increment


【解决方案1】:

我坚信,如果您使用 interlocked 来增加共享数据,那么您应该在访问该共享数据的任何地方使用 interlocked。同样,如果您使用在此处插入您最喜欢的同步原语来增加共享数据,那么您应该在您访问该共享数据的任何地方使用在此处插入您最喜欢的同步原语

int localNumberOfUpdates = Interlocked.CompareExchange(ref numberOfUpdates, 0, 0);

将为您提供您所寻找的。正如其他人所说,联锁操作是原子的。因此 Interlocked.CompareExchange 将始终返回最新的值。我一直使用它来访问简单的共享数据,例如计数器。

我对 Thread.VolatileRead 不太熟悉,但我怀疑它也会返回最新的值。如果只是为了保持一致,我会坚持使用联锁的方法。


附加信息:

我建议您查看 Jon Skeet 的回答,了解您可能想要回避 Thread.VolatileRead() 的原因:Thread.VolatileRead Implementation

Eric Lippert 在他的博客http://blogs.msdn.com/b/ericlippert/archive/2011/06/16/atomicity-volatility-and-immutability-are-different-part-three.aspx 中讨论了波动性和 C# 内存模型所做的保证。直截了当:“我不会尝试编写任何低锁代码,除了互锁操作的最琐碎用法。我将“易失性”的用法留给真正的专家。”

我同意 Hans 的观点,即该值将始终过时至少几个 ns,但如果您有一个不可接受的用例,它可能不太适合像 C# 这样的垃圾收集语言或非- 实时操作系统。 Joe Duffy 在这里有一篇关于联锁方法时效性的好文章:http://joeduffyblog.com/2008/06/13/volatile-reads-and-writes-and-timeliness/

【讨论】:

  • 我同意你关于在任何地方都坚持使用一个同步原语的观点。我看过很多真正知道他们在做什么的人的文章,他们说 volatile 的实际含义令人困惑。所以我的倾向是坚持使用 Interlocked 进行所有读取。但我不喜欢我必须使用 Interlocked.CompareExchange(ref foo,0,0) 而不是 Interlocked.Read(ref foo) 来“伪造”它,这从代码中清楚地表明我想要做的就是阅读。
  • 实际上,我只是查看了 Interlocked.cs 的 BCL。它看起来像 Interlocked.Read for longs is 实际上只是用 Interlocked.CompareExchange(ref location,0,0) 实现的。所以我想为整数做Interlocked.CompareExchange(ref foo,0,0) 应该没问题。如果有一个 int Interlocked.Read,那就是它的实现方式。
  • @Michael +1 检查 BCL。我之前看过 Interlocked.cs,但从未到定义 Interlocked.Read 的页面底部。我一直认为它像大多数其他成员一样有自己的外部实现。每天学习新东西。
  • 这是个好建议。关于易失性读取:易失性读取将为您提供最新的可用值然而 C# 语言不保证将观察到所有易失性读取和写入的单一规范排序由所有线程。请记住,“我刚刚读取了最新值”和“读取一个变量不会因写入不同变量而重新排序”之间存在很大差异,但许多人隐含地认为它们是相同的;他们绝对不是!
  • @markmnl 绝对正确。这完全取决于 CPU 架构的内存模型。 Interlocked 所做的只是在操作周围设置内存栅栏。在 x86 CPU(具有强大的内存模型)上,这恰好导致返回最新值。尝试在内存模型较弱的架构(例如 ARM、PowerPC)上使用Interlocked,在某些情况下可能会发生意想不到的事情。
【解决方案2】:

嗯,正如 Hans Passant 所说,您阅读的任何值都会有些陈旧。您只能控制保证 other 共享值与您刚刚读取的共享值一致,在代码中间使用内存栅栏读取多个没有锁的共享值(即:处于相同程度的“陈旧”)

栅栏还具有破坏某些编译器优化和重新排序的效果,从而防止在不同平台上的发布模式下出现意外行为。

Thread.VolatileRead 将导致发出一个完整的内存栅栏,这样就不会围绕您对 int 的读取(在读取它的方法中)重新排序读取或写入。显然,如果您只读取一个共享值(并且您没有读取其他共享值并且它们的顺序和一致性都很重要),那么它似乎没有必要......

但我认为无论如何你都需要它来击败编译器或 CPU 的一些优化,这样你就不会得到比必要的更“陈旧”的读取。

一个虚拟的 Interlocked.CompareExchange 将做与 Thread.VolatileRead 相同的事情(完全围栏和优化失败行为)。

CancellationTokenSource 使用的框架中遵循了一个模式 http://referencesource.microsoft.com/#mscorlib/system/threading/CancellationTokenSource.cs#64

//m_state uses the pattern "volatile int32 reads, with cmpxch writes" which is safe for updates and cannot suffer torn reads. 
private volatile int m_state;

public bool IsCancellationRequested
{
    get { return m_state >= NOTIFYING; } 
}

// ....
if (Interlocked.CompareExchange(ref m_state, NOTIFYING, NOT_CANCELED) == NOT_CANCELED) {
}
// ....

volatile 关键字具有发射“半”栅栏的效果。 (即:它阻止读取/写入在读取之前移动,并阻止读取/写入在写入之后移动)。

【讨论】:

    【解决方案3】:

    两者都可以工作吗(在提供最新价值的意义上,无论优化、重新排序、缓存等如何)?

    不,您获得的值总是过时。该值的陈旧程度是完全不可预测的。在绝大多数情况下,它会过时几纳秒,取决于你对价值采取行动的速度。但是没有合理的上限:

    • 当您的线程将另一个线程上下文切换到内核时,它可能会丢失处理器。典型的延迟约为 45 毫秒,没有确定的上限。这意味着您的进程中的另一个线程也被关闭,它可以继续驱动并继续改变值。
    • 就像任何用户模式代码一样,您的代码也会受到页面错误的影响。当处理器需要 RAM 用于另一个进程时发生。在可以并且将分页活动代码的重负载机器上。例如,鼠标驱动程序代码有时会出现冻结的鼠标光标。
    • 托管线程受到近乎随机的垃圾回收暂停。往往是较小的问题,因为很可能另一个正在改变值的线程也将被暂停。

    无论您如何处理该值,都需要考虑到这一点。也许不用说,这非常非常困难。实际例子很难找到。 .NET Framework 是一大块久经沙场的代码。您可以从the Reference Source 看到对 VolatileRead 用法的交叉引用。命中数:0。

    【讨论】:

    • 我的问题措辞不当。在时间方面,我不期望或要求对陈旧性设定硬上限。实际上,当我在 dev 中运行时,即使只是读取没有 Interlocked 或 Volatile 的 int 也不会导致问题。获得一个 100 毫秒过时的值对我来说并不是那么糟糕。我更关心一些奇怪的重新排序或优化,这会使我对 int 的读取被缓存而不是更新。所以我只想选择最好的、最惯用的 C# 方式来读取 int,因为它只使用 Interlocked.Increment 和 Interlocked.Decrement 进行更新。
    • OP 对我来说似乎很清楚:在时间点读取的值是最新的 int,它将反映在此之前对其所做的所有更新,即不在我们使用该值的时间 - 我们都知道没有理论上的上限。
    【解决方案4】:

    Thread.VolatileRead(numberOfUpdates) 是你想要的。 numberOfUpdatesInt32,因此默认情况下您已经具有原子性,Thread.VolatileRead 将确保处理波动性。

    如果 numberOfUpdates 被定义为 volatile int numberOfUpdates; 你不必这样做 这个,因为它的所有读取都已经是易失性读取。


    对于Interlocked.CompareExchange 是否更合适似乎存在混淆。请考虑以下两个文档摘录。

    来自Thread.VolatileRead 文档:

    读取字段的值。该值是计算机中任何处理器最新写入的值,与处理器数量或处理器缓存状态无关。

    来自Interlocked.CompareExchange 文档:

    比较两个 32 位有符号整数是否相等,如果相等,则替换其中一个值。

    就这些方法的规定行为而言,Thread.VolatileRead 显然更合适。您不想将numberOfUpdates 与另一个值进行比较,也不想替换它的值。你想读取它的值。


    Lasse 在他的评论中提出了一个很好的观点:使用简单的锁定可能会更好。当其他代码想要更新 numberOfUpdates 时,它会执行以下操作。

    lock (state)
    {
        state.numberOfUpdates++;
    }
    

    当你想阅读它时,你可以执行以下操作。

    int value;
    lock (state)
    {
        value = state.numberOfUpdates;
    }
    

    这将确保您对原子性和易变性的要求,而无需深入研究更晦涩、相对低级的多线程原语。

    【讨论】:

    • 但我认为 Interlocked.Read() 只适用于多头?不适用于整数。
    • 似乎有两个问题:防止撕裂和获取真正的最新值。 Interlocked.Read() 将解决这两个问题(据我所知),但仅限于长期。我没有撕裂问题(无论 x86 还是 x64),因为我有一个 int。但是我还有第二期读入最新值。
    • 我想确保我没有读取过时的缓存值,因为计数器在不同的 CPU 上递增。
    • @MichaelCovelli On x86 'Interlocked.CompareExchange' 实现为带有完整栅栏的 'CMPXCHG' 以防止重新排序。它将刷新存储缓冲区,但不会刷新缓存。缓存使用 MESI 来确保它不会读取 dirty 值。我不确定其他架构。
    • 这是正确的答案,许多人认为 Interlocked 是他们所需要的,但它只授予防止价值撕裂的原子性 - 它不保证你会得到最新的价值。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多