【问题标题】:How to make reading this instance primitive thread-safe without locking?如何在不锁定的情况下使读取此实例原始线程安全?
【发布时间】:2013-12-01 04:20:23
【问题描述】:

以下类的问题是在读取myThreadSafe.Value 时,它可能不会返回最新的值。

public class ThreadSafe
{   
    private int value;
    public int Value { get { return value; } }

    public void Update()
    {
        Interlocked.Add(ref value, 47); // UPDATE: use interlocked to not distract from the question being asked.
    }
}

我意识到我可以在读取和写入时锁定:

public int Value { get { lock(locker) return value; } }

public void Update()
{
    lock(locker)
    {
        value += 47;        
    }
}

而且我一直遵循这种使用锁的模式。但是,我正在尝试减少代码中的锁数量(有很多,而且它们被频繁调用,我已经分析过,Montior.Enter() 占用的时间比我想的要多 - 因为它被调用了很多次)。

更新:我现在想知道锁定是否确实会在确保我读取最新值方面产生任何影响,它仍然可能来自机器的 CPU 缓存之一不能它? (所有的锁保证都是互斥线程访问)。

我认为volatile 会是答案,MSDN 确实说:“这确保该字段中始终存在最新的值”,但是我读了elsewhere 写然后读使用 volatile 时仍然可以交换 CPU 指令,在这种情况下,我可以获得 myThreadSafe.Value 的先前值,也许我可以忍受 - 只有一次更新。

让我始终获得myThreadSafe.Value 最新值的最有效方法是什么?

更新:此代码将在 CPU 架构上编译和运行:

  • x86
  • AMD64(虽然我可以构建为 x86)
  • PowerPC
  • ARM(仅限小端)

使用运行时:

  • CLR v4.0
  • Mono(我不确定 Mono 运行时版本,但如果它们对应于 Mono 版本:至少 3.0)。

我希望对所有构建都使用相同的代码!

【问题讨论】:

  • 您多久写入一次值与读取一次?
  • Interlocked 的方法对你有用吗?
  • 我认为你需要看看ReaderWriterLock 类。在我看来,您有多个读者,但只有一个作者。
  • 我可以从 ThreadPool 中的任何线程写入,即调用 Update(),最多每秒 100 次。我将每秒阅读一个线程 60 次。
  • @HansPassant :) 我开始倾向于坚持使用锁定并减少调用次数,但是我现在担心锁定事件是否保证我正在读取相同的值?如果它被缓存在一个 CPU 中,它会使缓存失效吗?

标签: c# .net multithreading .net-4.0 thread-safety


【解决方案1】:

好的,我相信我找到了答案,我的担忧得到了证实!

代码恰好在 x86 和 AMD64 上是线程安全的,因为它们在写入变量时会使 CPU 缓存失效,从而导致后续读取从内存中读取变量。引用 Shafqay Ahmed quoting Jeffrey Richter:

由于两个处理器可以有不同的缓存,它们是内存的副本,它们可以有不同的值。在 x86 和 x64 中,处理器(根据 Jeffrey 的书)旨在同步不同处理器的缓存,因此我们可能看不到问题。

顺便说一句,使用lockInterlocked 会从缓存中刷新变量,因此在读取属性时使用锁定是安全的。来自http://blogs.msdn.com/b/ericlippert/archive/2011/06/16/atomicity-volatility-and-immutability-are-different-part-three.aspx

锁保证在锁内读取或修改的内存被观察到是一致的,锁保证一次只有一个线程访问给定的内存块,等等。

但是,当读取由另一个线程(不使用锁定同步结构)更新的值时,CLR 规范中没有保证将是最新的。事实上,在 ARM 上,我很可能使用 ThreadSafe 类从http://msdn.microsoft.com/en-us/magazine/jj553518.aspx 获得一个旧值:

如果您的代码依赖于依赖于 x86 CLR(而不是 ECMA CLR 规范)的实现的无锁算法,您需要将 volatile 关键字添加到适当的相关变量中。一旦您将共享状态标记为易失性,CLR 将为您处理所有事情。如果您像大多数开发人员一样,您已经准备好在 ARM 上运行,因为您已经使用锁来保护您的共享数据、正确标记 volatile 变量并在 ARM 上测试了您的应用程序。

所以看来答案是我可以在阅读时使用lock 或将我的字段设为volatile,尽管作为一名从事编译器工作的人@987654324,也许我应该使用锁定并尝试减少调用次数@:

锁太慢的情况很少,因为不了解确切的内存模型而导致代码错误的可能性很大。除了互锁操作的最琐碎用法之外,我不会尝试编写任何低锁代码。我将“易失性”的用法留给真正的专家。

【讨论】:

    【解决方案2】:

    我不确定您所说的“最新价值”是什么意思。您可以使用锁来确保您不会在写入Value 的同时读取它,这可能会产生一些奇怪的情况,但是如果您读取它然后写入它,您将无法获得最多日期值。

    要处理我提到的奇怪问题,您可以像以前那样使用锁。但你似乎想要一个不同的解决方案。如果您不想锁定读取,但您想确保写入是原子的,以便在多线程写入期间执行读取时读取不会返回奇数或其他一些混乱的事情,那么我会推荐使用Interlocked 类。

    简单地说:

    Interlocked.Add(ref value, 47);
    

    更多Interlocked功能可以在http://msdn.microsoft.com/en-us/library/system.threading.interlocked(v=vs.110).aspx找到

    这些函数在处理原语时非常有用。对于更复杂的对象,将需要其他解决方案,例如 ReaderWriterLockSlim 和其他解决方案。

    【讨论】:

    • 我的意思是读取时间点的最新值。像您一样使用 Interlocked 会给出相同的结果 - 在 Interlocked 执行了 Add 操作之后读取,即 myThreadSafe.Value 仍然可以在操作之前返回值!因为 value 可能位于 CPU 寄存器中(作为优化的结果),所以只知道调用 Update() 的线程和调用 myThreadSafe.Value 的另一个线程将从不同的位置读取 value。
    • @markmnl 所以你关心缓存的一致性。对于 x86 CPU,这是无关紧要的 - 一旦您写入值,它将使当前处理器和任何其他处理器上的缓存无效,迫使它们加载最新写入的值。 Interlocked 将起作用,lock 或任何其他 .Net 线程同步构造也将起作用。
    • 那么这是一个问题,我可以构建库目标并在其他 CPU 上运行
    • @markmnl 你打算这样做吗?如果是这样,这是您应该添加到问题中的重要信息。在非 x86 CPU 上运行 .Net 完全是另一种蠕虫。在这种情况下,您需要准确指定您计划在哪个 CPU 上运行您的代码,以及您将使用哪个 CLR 运行时(如 Mono)。每个不同的 CPU 和运行时都会有截然不同的方式来处理缓存一致性。如果您不指定此信息,那么人们会假设您的目标是 x86,它可能涵盖了 .Net 的 99% 的用例。
    • @markmnl 作为附加说明,lock 和 .Net 中的所有其他线程同步结构在这种情况下也可能不适用于其他 CPU。这不是孤立于 Interlocked 的东西。
    猜你喜欢
    • 2021-10-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-19
    • 2011-03-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多