【发布时间】:2018-12-13 07:33:26
【问题描述】:
据我了解,C# 是一种安全语言,除了通过 unsafe 关键字外,不允许访问未分配的内存。但是,它的内存模型允许在线程之间存在不同步访问时重新排序。这会导致竞争危险,在实例完全初始化之前,对新实例的引用似乎可用于竞争线程,并且是双重检查锁定的广为人知的问题。 Chris Brumme(来自 CLR 团队)在他们的 Memory Model 文章中解释了这一点:
考虑标准的双重锁定协议:
if (a == null)
{
lock(obj)
{
if (a == null)
a = new A();
}
}
这是一种在典型情况下避免锁定读取“a”的常用技术。它在 X86 上运行良好。但它会被 ECMA CLI 规范的合法但薄弱的实现所打破。确实,根据 ECMA 规范,获取锁具有获取语义,释放锁具有释放语义。
但是,我们必须假设在“a”的构建过程中发生了一系列商店。这些存储可以任意重新排序,包括将它们延迟到将新对象分配给“a”的发布存储之后的可能性。此时,在 store.release 之前有一个小窗口,表示离开锁。在该窗口内,其他 CPU 可以浏览引用“a”并查看部分构造的实例。
我一直对“部分构造的实例”的含义感到困惑。假设 .NET 运行时在分配时清除内存而不是垃圾回收 (discussion),这是否意味着其他线程可能会读取仍包含来自垃圾回收对象的数据的内存(如 what happens in unsafe languages)?
考虑以下具体示例:
byte[] buffer = new byte[2];
Parallel.Invoke(
() => buffer = new byte[4],
() => Console.WriteLine(BitConverter.ToString(buffer)));
上面有一个竞争条件;输出将是00-00 或00-00-00-00。但是,第二个线程是否有可能在数组的内存被初始化为 0 之前 读取对 buffer 的新引用,并改为输出一些其他任意字符串?
【问题讨论】:
-
是的。但请注意,那些其他线程将不得不忽略锁。我不会称这是一个很大的实际问题,它只会伤害“无锁”代码。当你认为你能做到这一点时,你可以处理这个小问题。这完全是关于一个假设的平台。
-
this article 建议 ECMA 不强制要求发布围栏。 ecma CLI F.4.1 含糊不清。当 Itanium 是一个 .NET 目标平台时,CLR 确实通过 ST.REL(ease) 而不是简单的 ST back 使用了释放栅栏,这是迄今为止唯一实际支持的具有弱内存模型架构的硬件。
-
是的,他是说内存内容可能看起来不是从此类处理器上的另一个线程初始化的。 IA64 给 Microsofties 带来了一段非常艰难的时期,我记得读到他们通过使每次内存读取获取和每次写入都释放来进行下注。嗯,这就是为什么抖动停止并且处理器超出生命支持的原因。他们确实对 ARM 抖动的内存模型进行了更改,除了“我们正在研究它”之外没有其他记录。他们的工作就是让它发挥作用,他们似乎做得很好,因为我还没有在 SO 上看到关于它的问题。
-
我应该注意到,ARM 是唯一在硬件中实现 C++11 内存模型的处理器设计。他们非常擅长让程序员开心,这是 IA64 严重错过的制胜策略。
标签: c# .net multithreading memory-barriers