内存重新排序会导致C#访问未分配的内存吗?

时间:2018-07-04 21:12:44

标签: c# .net multithreading memory-barriers

据我了解,C#是一种安全的语言,除了通过unsafe关键字之外,它不允许其他人访问未分配的内存。但是,当线程之间存在不同步的访问时,其内存模型允许重新排序。这会导致比赛危险,其中在实例完全初始化之前,新线程的引用似乎可用于赛车线程,并且这是众所周知的双重检查锁定问题。来自CLR团队的Chris Brumme在他们的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-0000-00-00-00。但是,第二个线程是否有可能在数组的内存已初始化为0之前,先读取对buffer 的新引用,而输出其他任意字符串呢?

1 个答案:

答案 0 :(得分:14)

让我们不要在这里埋葬:您的问题的答案是否,您将永远不会在CLR 2.0内存模型中观察到内存的预分配状态

我现在将解决您的几个非中心点。

  

据我了解,C#是一种安全的语言,除通过unsafe关键字外,不允许其他人访问未分配的内存。

这或多或少是正确的。有一些机制可以通过不使用unsafe来访问虚假内存-显然是通过非托管代码或滥用结构布局。但是总的来说,是的,C#是内存安全的。

  

但是,当线程之间存在不同步的访问时,其内存模型允许重新排序。

同样,这或多或少是正确的。考虑它的一种更好的方法是C#允许在某些约束下,对单个线程程序看不到重新排序的任何地方重新排序。这些限制包括在某些情况下引入获取和释放语义,以及在某些关键点保留某些副作用。

  

Chris Brumme(来自CLR团队)...

克里斯的晚期伟大文章是宝石,对CLR的早期发展提供了很多见识,但我注意到自2003年撰写该文章以来,内存模型有了一些增强,特别是关于您提出的问题。

克里斯说的对,双重检查锁定是非常危险的。有一种正确的方法可以对C#进行双重检查锁定,并且您甚至稍稍就会离开的 moment ,您陷入了只能重现的可怕错误中在弱内存模型硬件上。

  

这是否意味着另一个线程可能会读取仍包含来自垃圾收集对象的数据的内存

我认为您的问题不是专门针对Chris所描述的旧的弱ECMA内存模型,而是关于今天实际做出的保证。

重新排序无法显示对象的先前状态。您可以保证当您读取一个新分配的对象时,其字段全为零。

所有写入在当前内存模型中都具有释放语义,这使得这成为可能。详情请参见

http://joeduffyblog.com/2007/11/10/clr-20-memory-model/

将内存初始化为零的写操作不会相对于稍后的读取而及时向前移动。

  

我一直对“部分构造的对象”感到困惑

Joe在这里讨论:http://joeduffyblog.com/2010/06/27/on-partiallyconstructed-objects/

这里担心的不是我们可能会看到对象的预分配状态。相反,这里的担心是,当构造函数仍在另一线程上运行时,一个线程可能会看到对象

实际上,构造函数定型器有可能同时运行 ,这真是太奇怪了!由于这个原因,终结器很难正确编写。

采用另一种方式: CLR保证您将保留自己的不变式。 CLR的不变式是新分配的内存被清零,因此将保留不变式。

但是CLR与保存您的不变量有关!如果您有一个构造函数,当且仅当x为非空时,保证字段truey时, you 负责确保此不变式始终被认为是真实的。如果以某种方式this被两个线程观察到,则那些线程之一可能会观察到不变量被违反。