Windows 上*单 CPU*(多核)上的 C++ 多线程是否存在“缓存一致性”问题?

Ami*_*mit 2 c++ multithreading cpu-architecture memory-barriers stdatomic

(编辑:只是为了澄清:“缓存一致性”的问题是在不使用原子变量的情况下。)

是否有可能(单CPU情况:Windows可以在Intel / AMD / Arm CPU上运行),线程1运行在core-1上存储一个bool变量(例如)并且它保留在L1缓存中,而线程2在 core-n 上运行使用该变量,并且它会查找内存中该变量的另一个副本?

代码示例(为了演示该问题,我们假设这std::atomic_bool只是一个普通的bool):

#include <thread>
#include <atomic>
#include <chrono>

std::atomic_bool g_exit{ false }, g_exited{ false };

using namespace std::chrono_literals;

void fn()
{
    while (!g_exit.load(std::memory_order_acquire))
    {
        // do something (lets say it takes 1-4s, repeatedly)
        std::this_thread::sleep_for(1s);
    }

    g_exited.store(true, std::memory_order_release);
}

int main()
{
    std::thread wt(fn);
    wt.detach();

    // do something (lets say it took 2s)
    std::this_thread::sleep_for(2s);

    // Exit

    g_exit.store(true, std::memory_order_release);

    for (int i = 0; i < 5; i++) { // Timeout: 5 seconds.
        std::this_thread::sleep_for(1s);
        if (g_exited.load(std::memory_order_acquire)) {
            break;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 7

CPU 缓存在我们跨1运行 C++ 线程的核心之间始终是一致的,无论它们位于同一包(多核 CPU)中和/或分布在具有互连的套接字上。这使得一旦写入线程的存储已执行并提交到缓存就无法加载过时的值。作为执行此操作的一部分,它将向系统中的所有其他缓存发送无效请求。

其他线程最终总是可以看到您对变量的更新std::atomic,即使使用mo_relaxed. 这就是重点;std::atomic如果它不适合这个,那就毫无用处。(“最终”通常是大约 40 纳秒的线程间延迟;relaxed这并不更糟,它只是不会停止稍后内存操作的执行,直到存储对其他线程可见,就像seq_cst大多数 ISA 上需要的那样。 硬件是否会这样做?除了提供必要的保证之外,内存屏障还使原子操作的可见性更快?不,或者不显着)


但是如果没有std::atomic,您的代码将会非常糟糕,并且是MCU 编程的一个经典示例 - C++ O2 优化会中断 while 循环多线程程序陷入优化模式,但在 -O0 下正常运行- 编译器可以假设没有其他线程正在编写非-atomic var 它正在读取,因此它可以将实际负载提升出循环并将其保存在线程私有的 CPU寄存器中。所以它根本不会从一致性缓存中重新读取。即while(!exit_now){}成为if(!exit_now) while(1){}一个简单的bool exit_now全局。

寄存器是线程私有的,在任何方面都不连贯,因此即使在单处理器系统中,使用 plainint或编写的代码也可能会破坏这种方式。bool上下文切换只是将寄存器保存/恢复到线程私有内核缓冲区,它们不知道代码使用寄存器的目的,因此永远不会产生bool g_exit从内存重新读取到线程寄存器的效果。while(!non_atomic_flag){}事实上,代码在优化后甚至可能不会重新检查寄存器if(!non_atomic_flag) while(42){}

(除了你的sleep_for调用可能会阻止这种优化。它可能没有被声明为纯的,因为你不希望编译器优化对它的多次调用;时间是副作用。所以编译器必须假设对它的调用可以修改全局变量,因此会从内存中重新读取全局变量(使用通过一致缓存的正常加载指令))。

另相关:如果使用“memory_order_relaxed”检查,为什么要使用“memory_order_seq_cst”设置停止标志?


std::thread脚注 1:仅支持在同一一致性域中跨内核运行的C++ 实现。在几乎所有系统中,只有一个一致性域包含所有套接字中的所有核心,但节点之间具有非一致性共享内存的大型集群是可能的。

具有共享内存但与 ARM DSP 内核不一致的ARM 微控制器内核的嵌入式板也是如此。您不会在这两个内核上运行单个操作系统,也不会考虑在同一 C++ 程序的这些不同内核上运行的代码。

有关缓存一致性的更多详细信息,请参阅何时在多线程中使用 易失性?