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

C# 我可以避免对很少变化的变量使用锁吗?,c#,multithreading,concurrency,volatile,C#,Multithreading,Concurrency,Volatile,我一直在读Joe Duffy关于并发编程的书。我有一个关于无锁线程的学术问题 第一:我知道无锁线程充满了危险(如果你不相信我,请阅读书中关于内存模型的部分) 不过,我有一个问题: 假设我有一个带有int属性的类 多个线程将非常频繁地读取此属性引用的值 值会发生更改的情况极为罕见,当值发生更改时,只有一个线程会对其进行更改 如果在另一个使用它的操作运行时它确实发生了变化,那么没有人会失去一根手指(使用它的人做的第一件事就是将它复制到局部变量) 我可以使用锁(或者readerwriterlocksl

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

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

不过,我有一个问题: 假设我有一个带有int属性的类

多个线程将非常频繁地读取此属性引用的值

值会发生更改的情况极为罕见,当值发生更改时,只有一个线程会对其进行更改

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

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

然而,即使是volatile也会对性能造成影响

如果我在VolatileWrite发生变化时使用它,并让读取保持正常访问,该怎么办。大概是这样的:

public class MyClass
{
  private int _TheProperty;
  internal int TheProperty
  {
    get { return _TheProperty; }
    set { System.Threading.Thread.VolatileWrite(ref _TheProperty, value); }
  }
}
a = !a;
我不认为我会在现实生活中尝试这一点,但我对答案很好奇(最重要的是,作为我是否理解我一直在阅读的内存模型的检查点)。

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

因为您说过只有一个线程向它写入,所以您永远不应该对正确的值有争用,并且只要您正在缓存本地副本,就永远不会得到脏数据

但是,如果操作系统线程正在执行更新,您可能需要声明它

还要记住,某些操作不是原子操作,如果有多个写入程序,可能会导致问题。例如,如果有多个编写器,即使
bool
类型不会损坏,如下语句:

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

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

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

问题是阅读线程是否会看到更改。这不仅仅是它是否立即看到的问题

坦率地说,我已经放弃了试图理解波动性——我知道这并不完全是我过去认为的那样。。。但我也知道,如果在读取线程上没有任何内存障碍,您可能永远都在读取相同的旧数据。

volatile的“性能冲击”是因为编译器现在生成代码来实际检查值,而不是优化值-换句话说,无论你做什么,你都必须承受这种性能损失。

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

我强烈建议阅读免费提供的论文《每个程序员都应该知道关于内存的知识》。很多魔法在引擎盖下进行,主要是防止击中自己的脚。

将变量标记为“volatile”有两种效果

1) 读和写具有获取和释放语义,因此其他内存位置的读和写不会相对于该内存位置的读和写“在时间上向前和向后移动”。(这是一种简化,但你接受我的观点。)

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

我不知道前一点是否与你的情况有关;您只描述了一个内存位置。只有易失性写入而不是易失性读取是否重要取决于您自己

但在我看来,后一点似乎非常相关。如果在非易失性变量上有自旋锁:

while(this.prop == 0) {}
jitter在其权限范围内生成此代码,就像您编写的代码一样

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

不管它是否真的这样做,我不知道,但它有权利这样做。如果您希望代码在循环的每次循环中都重新检查属性,将其标记为volatile是正确的方法。

在CPU级别,是的,每个处理器最终都会看到内存地址的更改。即使没有锁或内存障碍。锁和屏障只会确保它以相对顺序(w.r.t其他指令)发生,这样它对您的程序来说似乎是正确的

问题不在于缓存一致性(我希望Joe Duffy的书不会犯这个错误)。缓存保持一致性——只是这需要时间,处理器不必等待这种情况发生——除非您强制执行。因此,处理器将继续执行下一条指令,这可能会也可能不会在前一条指令之前发生(因为每个内存读/写操作都需要不同的时间。具有讽刺意味的是,由于处理器需要在一致性等方面达成一致的时间),这会导致某些缓存线的一致性比其他缓存线更快(即,根据行是否被修改、独占、共享或无效,进入必要状态需要或多或少的工作。)

因此,读取可能看起来很旧或来自过时的缓存,但实际上它只是发生在