c ++ 11注册缓存线程安全

jas*_* na 6 c++ multithreading volatile cpu-registers c++11

在volatile中:多线程程序员最好的朋友,Andrei Alexandrescu给出了这个例子:

class Gadget
{
public:
    void Wait()
    {
        while (!flag_)
        {
            Sleep(1000); // sleeps for 1000 milliseconds
        }
    }
    void Wakeup()
    {
        flag_ = true;
    }
    ...
private:
    bool flag_;
};
Run Code Online (Sandbox Code Playgroud)

他说,

... ...编译器得出结论,它可以将flag_缓存在寄存器中...它会损害正确性:在您调用Wait for some Gadget对象之后,虽然另一个线程调用Wakeup,但Wait将永远循环.这是因为flag_的更改不会反映在缓存flag_的寄存器中.

然后他提供了一个解决方案

如果对变量使用volatile修饰符,则编译器不会将该变量缓存在寄存器中 - 每次访问都将访问该变量的实际内存位置.

现在,其他人在stackoverflow和其他地方提到volatile关键字并不真正提供任何线程安全保证,我应该使用std :: atomic或mutex同步,我同意.

但是,以std :: atomic路由为例,内部使用内存栅栏read_acquire和write_release(Acquire和Release Semantics),我看不出它是如何实际修复寄存器缓存问题的.

例如,在x86的情况下,x86/64上的每个加载都意味着获取语义,并且每个存储意味着释放语义,使得x86下的编译代码根本不会发出任何实际的内存屏障.(C++ 11中memory_order_consume的目的)

g = Guard.load(memory_order_acquire);
if (g != 0)
    p = Payload;
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

在Intel x86-64上,Clang编译器为此示例生成紧凑的机器代码 - 每行C++源代码一个机器指令.该系列处理器具有强大的内存模型,因此编译器无需发出特殊的内存屏障指令即可实现读取.

所以....现在假设x86 arch,std :: atomic如何解决注册表问题中的缓存?在编译代码中没有用于读取的内存屏障指令,它似乎与常规读取的编译代码相同.

Dav*_*rtz 5

您是否注意到代码中只有一个寄存器没有负载?来自的显式内存负载_Guard.事实上它确实阻止了寄存器中的缓存.

现在它如何做到这取决于特定平台的实现std::atomic,但它必须这样做.

顺便说一句,亚历山大夫斯库的推理对于现代平台来说是完全错误的.虽然确实可以volatile防止编译器在寄存器中进行高速缓存,但它并不能阻止CPU或硬件进行类似的高速缓存.在某些平台上,它可能恰好是足够的,但是当完全可移植的替代方案随时可用时,绝对没有理由编写可能会破坏未来CPU,编译器,库或平台的无法使用的非可移植代码.

  • @ user3101492不,因为没有`std :: atomic`,它可以缓存在寄存器中.碰巧的是,x86硬件由于其兼容性追溯到8088而竭尽全力使多线程代码"正常工作",但它无法对编译器优化做任何事情. (3认同)