代码之家  ›  专栏  ›  技术社区  ›  Douglas

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

  •  13
  • Douglas  · 技术社区  · 6 年前

    据我所知,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 . 但是,第二个线程是否可能读取对 buffer 之前 数组的内存已初始化为0,并输出其他任意字符串?

    1 回复  |  直到 6 年前
        1
  •  14
  •   Eric Lippert    6 年前

    我们不要在这里埋怨别人:你的问题的答案是 不,在clr 2.0内存模型中永远不会看到内存的预分配状态 是的。

    我现在谈谈你的几个非中心观点。

    据我所知,c是一种安全的语言,除了通过unsafe关键字之外,不允许任何人访问未分配的内存。

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

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

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

    克里斯·布鲁姆(来自CLR团队)。

    已故伟大的克里斯的文章是瑰宝,给了我们很多关于clr早期的见解,但我注意到,自从2003年那篇文章被写出来以来,内存模型已经有了一些增强,特别是关于你提出的问题。

    克里斯是对的,双重检查锁是超级危险的。有一种正确的方法可以在C中执行双重检查锁定,并且 瞬间 你甚至离开它 轻微地 ,您将陷入只在弱内存模型硬件上重新编程的可怕错误的杂草中。

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

    我认为你的问题并不是关于chris所描述的旧的弱ecma内存模型,而是关于今天实际做了什么保证。

    重新排序不可能公开对象的以前状态 . 当您读取一个新分配的对象时,它的字段都是零。

    这是因为所有写操作在当前内存模型中都具有释放语义;有关详细信息,请参见:

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

    将内存初始化为零的写入将不会在以后的读取中及时向前移动。

    我总是被“部分构造的物体”搞糊涂

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

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

    事实上, 构造器 以及 终结器 跑步 同时 ,太奇怪了!由于这个原因,终结器很难正确编写。

    换一种说法: clr保证它自己的不变量将被保留 是的。CLR的一个不变量是观察到新分配的内存被调零,这样不变量将被保留。

    但CLR不是为了保护 你的 不变量!如果您有一个保证该字段的构造函数 x true 如果且仅当 y 则为非空 负责确保始终观察到该不变量为真。如果以某种方式 this 由两个线程观察,则其中一个线程可能会观察到违反的不变量。