52

(这是重复:如何正确读取 Interlocked.Increment'ed int 字段?但是,在阅读了答案和评论后,我仍然不确定正确的答案。)

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

Interlocked.Increment(ref numberOfUpdates);

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

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

或者

int localNumberOfUpdates = Thread.VolatileRead(numberOfUpdates);

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

4

5 回答 5

49

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

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

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

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


附加信息:

我建议您查看 Jon Skeet 的回答,了解为什么您可能想要回避 Thread.VolatileRead():Thread.VolatileRead 实现

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

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

于 2014-07-22T16:50:16.753 回答
6

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;
}

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

于 2014-07-17T16:07:16.643 回答
3

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

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

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

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

于 2014-07-22T17:37:38.060 回答
3

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

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

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 关键字具有发出“半”栅栏的效果。(即:它阻止读取/写入在读取之前移动,并阻止读取/写入在写入之后移动)。

于 2014-07-22T18:22:01.653 回答
0

不知道为什么没有人提到Interlocked.Add(ref localNumberOfUpdates, 0),但对我来说似乎是最简单的方法......

于 2022-03-02T14:53:17.977 回答