【问题标题】:Does a MemoryBarrier guarantee memory visibility for all memory?MemoryBarrier 是否保证所有内存的内存可见性?
【发布时间】:2016-09-20 19:46:33
【问题描述】:

如果我理解正确,在 C# 中,lock 块保证对一组指令的独占访问,但它也保证从内存中读取的任何内容都会反映任何 CPU 缓存中该内存的最新版本。我们将lock 块视为保护块内读取和修改的变量,这意味着:

  1. 假设您已在必要时正确实施锁定,则这些变量一次只能由一个线程读取和写入,并且
  2. lock 块中的读取会看到变量的最新版本,lock 块中的写入对所有线程都可见。

(对吗?)

第二点是我感兴趣的。是否有某种魔法可以保证只有在受lock 块保护的代码中读取和写入的变量 是新鲜的,或者在lock 的实现中使用的内存屏障保证所有 内存现在对所有线程都一样新鲜?请原谅我对缓存如何工作的心理模糊,但我读过缓存包含几个多字节“行”数据。我想我要问的是,内存屏障是否强制同步所有“脏”缓存行或只是一些,如果只是一些,是什么决定了哪些行得到同步?

【问题讨论】:

    标签: c# multithreading


    【解决方案1】:

    如果我理解正确,在 C# 中,锁块保证对一组指令的独占访问...

    没错。该规范保证了这一点。

    但它也保证从内存中读取的任何内容都会反映任何 CPU 缓存中该内存的最新版本。

    C# 规范对“CPU 缓存”只字未提。你已经离开了规范保证的领域,进入了实现细节的领域。没有要求 C# 的实现必须在具有任何特定缓存架构的 CPU 上执行。

    是否有一些魔法可以保证只有在受锁块保护的代码中读取和写入的变量是新鲜的,或者在锁的实现中使用的内存屏障是否保证所有内存现在对于所有线程都是同样新鲜的?

    与其尝试解析您的非此即彼的问题,不如说一下语言实际保证的内容。一个特殊的效果是:

    • 对变量的任何写入,无论是否为易失性
    • 对 volatile 字段的任何读取
    • 任意投掷

    在某些特殊点保留特殊效果的顺序:

    • 易失性字段的读写
    • 线程创建和终止

    运行时需要确保特殊效果与特殊点的顺序一致。因此,如果在锁定之前读取 volatile 字段,之后进行写入,则在写入之后无法移动读取。

    那么,运行时如何实现这一点?打败了我。但是运行时肯定不需要“保证所有线程的所有内存都是新鲜的”。运行时需要确保某些读取、写入和抛出相对于特殊点按时间顺序发生,仅此而已。

    运行时特别要求所有线程都遵守相同的顺序

    最后,我总是通过指向您这里来结束这类讨论:

    http://blog.coverity.com/2014/03/26/reordering-optimizations/

    读完之后,您应该对即使在 x86 上当您随意省略锁时也可能发生的各种可怕事情有所了解。

    【讨论】:

    • 事实上,C# 规范对内存模型的描述太少了,而 CLI 规范则为真正疯狂的情况敞开了大门,在这种情况下,甚至局部变量都不能被信任以在两次读取之间保持其值。我希望我们可以改进很多,但不要在非常不久的将来寻找合理的内存模型......
    • 就像我在对 Jon 的回答的评论中所说的那样,我还没有完全遵循这一点,但我可以接受它是正确的并仔细研究它。感谢您的回复!
    • 这应该会有所帮助。锁获得两个半栅栏(开始时 1 个,结束时 1 个)。为了简单起见,我们假设它是一个完整的围栏。以下是在 CPU 级别发生的情况:“此序列化操作可确保在程序顺序中 MFENCE 指令之前的每个加载和存储指令都是全局可见的,然后 MFENCE 指令之后的任何加载或存储指令都是全局可见的。” x86.renejeschke.de/html/file_module_x86_id_170.html
    【解决方案2】:

    锁块内的读取会看到变量的最新版本,锁块内的写入对所有线程都是可见的。

    不,这绝对是一种有害的过度简化。

    当您输入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;
    }
    

    现在它比这更微妙,但这就是使用公共锁来保护一组变量有效的原因。

    【讨论】:

    • 所以锁末尾的内存栅栏使写入对主内存可见,但是另一个线程需要一个内存栅栏来保证它读取的是未缓存的值? (对不起,如果我在扼杀你所说的话。仍然模糊。)
    • @adv12:与其推理 CPU 级别实际发生的事情,不如考虑保证什么。在 C 中读取 x 不是“特殊事件”,因此它可以相对于任何其他读取和写入任意重新排序。 (嗯,除了用 C 写的!显然那里存在数据依赖关系。)
    • @adv12:关于重新排序读取(和写入)的业务是关键,也是最终非常令人困惑的地方。基本上,除非有任何保证,否则假设您执行的任何读取实际上可能已经在代码中更早地执行。我不是这方面的专家——很少有人是这方面的专家——我只是尽量坚持保证安全的事情。
    • @adv12:还有,我和 Jon 在一起。我认为可以肯定地说我们两个都非常了解 C#,但我对哪些内存模型允许哪些优化的了解并不是很好。我避免在真正的代码中使用这些东西,比如瘟疫;能避免就不要写多线程代码,不能避免就拿锁。
    • @EricLippert:虽然如果你把它发挥到极致,你根本不能保证:void Foo(string x) { if (x == null) { throw new NullArgumentException(); } int y = x.Length; } could 由于 JIT 内联,仍然会抛出,省略参数并引入额外的字段读取。这就是我想与工作组一起解决的问题:)
    猜你喜欢
    • 2017-08-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多