MemoryBarrier保证所有内存的可见性吗?
本文关键字:可见性 内存 MemoryBarrier | 更新日期: 2023-09-27 18:08:02
如果我理解正确的话,在c#中,lock
块保证了对一组指令的独占访问,但它也保证了从内存中读取的任何数据都反映了CPU缓存中该内存的最新版本。我们认为lock
块保护在块内读取和修改的变量,这意味着:
- 假设您已经在必要的地方正确地实现了锁,这些变量一次只能由一个线程读写,并且
-
lock
块内的读取看到变量的最新版本,lock
块内的写入对所有线程可见。
(对吗?)
我感兴趣的是第二点。是否有某种魔力,只有在lock
块保护的代码中读写的变量才保证是新鲜的,或者在lock
实现中使用的内存屏障保证所有内存现在对所有线程都是同样新鲜的?请原谅我在这里对缓存的工作方式感到模糊,但是我读到过缓存保存了几个多字节的"行"数据。我想我要问的是,内存屏障是否强制同步所有"脏"缓存行,或者只是一些,如果只是一些,是什么决定了哪些行被同步?
如果我理解正确的话,在c#中,锁块保证了对一组指令的独占访问…
。规范保证。
,但它也保证从内存中读取的任何数据都反映了该内存在任何CPU缓存中的最新版本。
c#规范对"CPU缓存"只字未提。您已经离开了规范所保证的领域,而进入了实现细节的领域。c#的实现并不要求在具有任何特定缓存架构的CPU上执行。
是否有一些魔力,只有在锁块保护的代码中读写的变量才保证是新鲜的,或者在锁的实现中使用的内存屏障保证所有线程的所有内存现在都是同样新鲜的?
与其解析非此即彼的问题,不如让我们来看看语言实际上保证了什么。一个特殊效果是:
- 对变量的任何写入,无论是否为易失性
- volatile字段的任何读取
- 任何把
特殊效果的顺序在某些特殊点保留:
- volatile字段的读写 <
- 锁/gh>
- 线程创建和终止
运行时需要确保特殊效果与特殊点的顺序一致。因此,如果在锁之前有一个易失性字段的读操作,在锁之后有一个写操作,那么这个读操作就不能在写操作之后移动。
那么,运行时是如何实现的呢?我也不知道。但是运行时当然不需要"保证所有线程的所有内存都是新鲜的"。运行时需要确保特定的读、写和抛出按照特定点的时间顺序发生,仅此而已。
运行时特别不要求所有线程遵守相同的顺序。
最后,我总是在结束这类讨论时指出:
http://blog.coverity.com/2014/03/26/reordering-optimizations/
读完这篇文章后,您应该对即使在x86上随意省略锁也可能发生的各种可怕的事情有所了解。
锁块内的读操作可以看到变量的最新版本,而锁块内的写操作对所有线程都是可见的。
不,这绝对是有害的过度简化。
当你输入lock
语句时,有一个内存栅栏,其中有点像意味着你总是读取"新鲜"数据。当你退出lock
状态时,有一个内存栅栏,有点像意味着你写的所有数据都被保证写入主存,并且可供其他线程使用。
如果你的代码读写变量而没有取锁,那么就不能保证它会"看到"由行为良好的代码(即使用锁的代码)写的数据,或者行为良好的线程会"看到"由错误代码写的数据。
例如:
private readonly object padlock = new object();
private int x;
public void A()
{
lock (padlock)
{
// Will see changes made in A and B; may not see changes made in C
x++;
}
}
public void B()
{
lock (padlock)
{
// Will see changes made in A and B; may not see changes made in C
x--;
}
}
public void C()
{
// Might not see changes made in A, B, or C. Changes made here
// might not be visible in other threads calling A, B or C.
x = x + 10;
}
现在它比那更微妙,但这就是为什么使用一个公共锁来保护一组变量。