我可以避免为我很少变化的变量使用锁吗?

时间:2010-01-28 21:14:52

标签: c# multithreading concurrency volatile

我一直在阅读Joe Duffy关于并发编程的书。我有一个关于无锁线程的学术问题。

首先:我知道无锁线程充满了危险(如果你不相信我,请阅读书中关于内存模型的部分)

尽管如此,我有一个问题: 假设我有一个带有int属性的类。

多个线程

将非常频繁地读取此属性引用的值

极值很少会改变,当它发生变化时,它将是一个改变它的线程。

如果它确实发生了变化,而另一个使用它的操作正在飞行中,那么没有人会失去一个手指(任何人使用它的第一件事就是将它复制到局部变量)

我可以使用锁(或readerwriterlockslim来保持读取并发)。 我可以标记变量volatile(很多例子都是这样做的)

然而,即使是不稳定也可能会影响性能。

如果我在更改时使用VolatileWrite,并保持读取的访问正常,该怎么办?像这样:

public class MyClass
{
  private int _TheProperty;
  internal int TheProperty
  {
    get { return _TheProperty; }
    set { System.Threading.Thread.VolatileWrite(ref _TheProperty, value); }
  }
}

我不认为我会在现实生活中尝试这一点,但我对答案感到好奇(最重要的是,作为我是否理解我一直在阅读的记忆模型内容的检查点)。 / p>

7 个答案:

答案 0 :(得分:6)

将变量标记为“volatile”有两个影响。

1)读取和写入具有获取和释放语义,因此对于该存储器位置的读取和写入,其他存储器位置的读取和写入将不会“及时向前和向后移动”。 (这是一种简化,但你明白我的意思。)

2)抖动生成的代码不会“缓存”似乎在逻辑上不变的值。

前一点是否与您的情景相关,我不知道;你只描述了一个内存位置。是否只有易失性写入但不是易失性读取是很重要的,由您决定。

但在我看来,后一点非常重要。如果您对非易失性变量进行自旋锁定:

while(this.prop == 0) {}

jitter在生成此代码的权利范围内,就像您编写的那样

if (this.prop == 0) { while (true) {} }

是否真的这样做,我不知道,但它有权利。如果你想要的是代码实际上重新检查每个循环的属性,将它标记为volatile是正确的方法。

答案 1 :(得分:4)

问题在于阅读主题是否永远看到了变化。这不仅仅是它是否立即看到它

坦率地说,我已经放弃了试图理解波动性 - 我知道这并不意味着我曾经想过的......但我也知道,在阅读线上没有任何记忆障碍,你可以永远地阅读相同的旧数据。

答案 2 :(得分:2)

volatile的“性能影响”是因为编译器现在生成代码来实际检查值而不是优化它 - 换句话说,你将 来获取该值无论你做什么,性能都会受到影响。

答案 3 :(得分:2)

在CPU级别,是的每个处理器最终都会看到对内存地址的更改。即使没有锁或内存障碍。锁和障碍只会确保所有这些都发生在相对排序中(例如其他说明),以使其对您的程序来说是正确的。

问题不在于缓存一致性(我希望Joe Duffy的书不会犯这个错误)。缓存保持一致 - 这只是需要时间,并且处理器不愿意等待这种情况发生 - 除非你强制执行它。因此,处理器继续执行下一条指令,该指令可能会或可能不会在之前发生前一条指令(因为每次内存读/写make都需要不同的时间。讽刺的是因为处理器同意一致性等的时间 - 这会导致一些高速缓存行比其他高速缓存更快(即取决于行是修改,独占,共享还是无效它需要更多或更少)努力进入必要的状态)。)

因此,读取可能显示为旧的或来自过时的缓存,但实际上它只是比预期更早发生(通常是因为前瞻和分支预测)。当真正 读取时,缓存是连贯的,从那时起它就刚刚改变了。所以当你阅读它时,它的价值并不老,但现在你需要的时候就是它。你刚刚读完它。 : - (

或等效地,它的编写时间晚于您的代码逻辑,认为它将被编写。

或两者兼而有之。

无论如何,如果这是C / C ++,即使没有锁/障碍,你也最终获取更新的值。 (通常在几百个周期内,因为内存耗时很长)。在C / C ++中,您可以使用volatile(弱非线程易失性)来确保不从寄存器中读取值。 (现在有一个非连贯的缓存!即寄存器)

在C#中,我不太了解CLR知道一个值可以在寄存器中保留多长时间,也不知道如何确保从内存中真正重新读取。你已经失去了'弱'的波动。

我怀疑只要变量访问没有完全被编译掉,你最终会用完寄存器(x86没有很多东西可以开始)并重新读取。

但我不能保证。如果你可以将你的volatile读取限制在你的代码中的某个特定点,这通常但不常见(即在一段时间(thing_to_do)循环中开始下一个任务)那么这可能是你能做到的最好的。

答案 4 :(得分:1)

这是我在“最后一个作家获胜”模式适用于该情况时使用的模式。我使用了volatile关键字,但在Jeffery Richter的代码示例中看到此模式后,我开始使用它。

答案 5 :(得分:1)

对于正常事物(如内存映射设备),在CPU / CPU内部/之间进行的缓存一致性协议是为了确保共享该内存的不同线程获得一致的事物视图(即,如果我更改在一个CPU中的内存位置的值,被其缓存中具有内存的其他CPU看到。在这方面,volatile将有助于确保优化器不会通过读取寄存器中缓存的值来优化内存访问(无论如何总是通过缓存)。 C#文档似乎很清楚。同样,应用程序员通常不必自己处理缓存一致性。

我强烈建议您阅读免费提供的论文“每个程序员应该了解的内存”。很多魔法在引擎盖下进行,主要是防止自己在脚下射击。

答案 6 :(得分:0)

在C#中,int类型是线程安全的。

既然你说只有一个线程写入它,你就不应该争论什么是正确的值,只要你正在缓存本地副本,就不应该得到脏数据。

但是,如果操作系统线程正在进行更新,您可能希望声明它volatile

另请注意,某些操作不是 atomic ,如果您有多个编写器,则可能会导致问题。例如,即使bool类型不会因为有多个编写器而损坏,也可以这样声明:

a = !a;

不是原子的。如果两个线程同时读取,则表示存在竞争条件。