【问题标题】:How do threaded systems cope with shared data being being cached by different cpus?线程系统如何处理被不同 CPU 缓存的共享数据?
【发布时间】:2009-07-09 17:52:43
【问题描述】:

我主要来自 c++ 背景,但我认为这个问题适用于任何语言的线程。这是场景:

  1. 我们有两个线程(ThreadA 和 ThreadB),共享内存中有一个值 x

  2. 假设对 x 的访问由互斥锁(或其他合适的同步控制)适当控制

  3. 如果线程碰巧在不同的处理器上运行,如果 ThreadA 执行写操作,但它的处理器将结果放在它的 L2 缓存而不是主存中,会发生什么情况?那么,如果 ThreadB 尝试读取该值,它会不会只是查看自己的 L1/L2 缓存/主内存,然后使用那里的旧值?

如果不是这样,那么如何处理这个问题?

如果是这样的话,那有什么办法呢?

【问题讨论】:

    标签: multithreading caching multiprocessor


    【解决方案1】:

    你的例子可以正常工作。

    多个处理器使用coherency protocol(例如MESI)来确保数据在缓存之间保持同步。使用 MESI,每个高速缓存行都被视为已修改、独占、在 CPU 之间共享或无效。写入在处理器之间共享的高速缓存行会强制它在其他 CPU 中变为无效,从而保持高速缓存同步。

    但是,这还不够。不同的处理器有不同的memory models,并且大多数现代处理器都支持某种程度的重新排序内存访问。在这些情况下,需要memory barriers

    例如,如果您有线程 A:

    DoWork();
    workDone = true;
    

    还有线程 B:

    while (!workDone) {}
    DoSomethingWithResults()
    

    由于两者都在不同的处理器上运行,因此无法保证在对 workDone 和 DoSomethingWithResults() 的写入以可能不一致的状态继续之前,线程 B 可以看到在 DoWork() 中完成的写入。内存屏障保证读取和写入的某些顺序 - 在线程 A 中的 DoWork() 之后添加内存屏障将强制 DoWork 完成的所有读取/写入在写入 workDone 之前完成,以便线程 B 获得一致的视图。互斥体本身就提供了内存屏障,因此读/写操作无法传递锁定和解锁调用。

    在您的情况下,一个处理器会向其他处理器发出信号,表明它弄脏了缓存线并强制其他处理器从内存中重新加载。获取互斥体以读取和写入值可确保对内存的更改按预期顺序对其他处理器可见。

    【讨论】:

    • 非常感谢您的回复。我想知道某种硬件级别的机制是否必须在这里发挥作用,因为在语言/编译器级别可以完成的工作似乎存在实际限制。
    【解决方案2】:

    像互斥锁这样的大多数锁定原语都隐含memory barriers。这些会强制进行缓存刷新和重新加载。

    例如,

    ThreadA {
        x = 5;         // probably writes to cache
        unlock mutex;  // forcibly writes local CPU cache to global memory
    }
    ThreadB {
        lock mutex;    // discards data in local cache
        y = x;         // x must read from global memory
    }
    

    【讨论】:

    • 我不相信障碍会强制缓存刷新,它们会强制限制内存操作的顺序。如果对 X 的写入可以通过解锁互斥锁,则缓存刷新将无济于事。
    • 如果编译器对它们的内存操作重新排序,屏障将毫无用处,嗯?至少对于 GCC,我认为这通常是用内存破坏器实现的,它告诉 GCC “使任何关于内存的假设无效”。
    • 哦,我明白你在说什么。只要在处理器之间正确遵守排序,就不需要缓存刷新。所以我想这个解释是一个简化的视图,你的解释更多的是硬件细节。
    • 然而,就程序员而言,“这块内存必须从这个处理器的缓存”和“这块内存的全局状态现在已设置”。
    • 对。我认为您不希望在这里进行缓存刷新,因为希望常见的情况是内存实际上没有在处理器之间共享。在 x86 上,内存屏障通常只是一个 lock xchg 指令,我认为这对缓存没有任何影响。
    【解决方案3】:

    一般来说,编译器理解共享内存,并花费相当大的努力来确保将共享内存放置在可共享的位置。现代编译器在排序操作和内存访问的方式上非常复杂。他们倾向于理解线程和共享内存的本质。这并不是说它们是完美的,但总的来说,编译器会处理大部分问题。

    【讨论】:

      【解决方案4】:

      C# 有一些内置支持此类问题。 您可以使用 volatile 关键字标记变量,这会强制它在所有 cpu 上同步。

      public static volatile int loggedUsers;
      

      另一部分是 .NET 方法的语法包装器,称为 Threading.Monitor.Enter(x) 和 Threading.Monitor.Exit(x),其中 x 是要锁定的变量。这会导致其他试图锁定 x 的线程必须等到锁定线程调用 Exit(x)。

      public list users;
      // In some function:
      System.Threading.Monitor.Enter(users);
      try {
         // do something with users
      }
      finally {
         System.Threading.Monitor.Exit(users);
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-11-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-06-23
        相关资源
        最近更新 更多