【问题标题】:Memory model ordering and visibility?内存模型排序和可见性?
【发布时间】:2011-11-19 15:52:56
【问题描述】:

我试着寻找这方面的细节,我什至阅读了关于互斥锁和原子的标准......但我仍然无法理解 C++11 内存模型可见性保证。 据我了解,互斥互斥的非常重要的特性是确保可见性。 Aka每次只有一个线程增加计数器是不够的,重要的是线程增加最后使用互斥锁的线程存储的计数器(我真的不知道为什么人们在讨论时不提这个互斥体,也许我有不好的老师:))。 因此,据我所知, atomic 不会强制立即可见: (来自维护 boost::thread 并实现了 c++11 线程和互斥库的人):

带有 memory_order_seq_cst 的栅栏不强制立即执行 对其他线程的可见性(MFENCE 指令也没有)。 C++0x 内存排序约束就是这样 --- 排序 约束。 memory_order_seq_cst 操作形成一个总顺序,但是 该命令是什么没有限制,除了它必须 所有线程都同意,并且不能违反其他顺序 约束。特别是,线程可能会继续看到“陈旧”的值 有一段时间,只要他们看到的值的顺序与 约束。

我同意。但问题是我很难理解关于原子的 C++11 构造是“全局的”,并且只能确保原子变量的一致性。 特别是,我了解以下哪些(如果有)内存排序保证在加载和存储之前和之后会有一个内存栅栏: http://www.stdthread.co.uk/doc/headers/atomic/memory_order.html

据我所知,std::memory_order_seq_cst 插入了内存屏障,而其他只强制对某些内存位置的操作进行排序。

所以有人可以澄清一下吗,我想很多人会使用 std::atomic 制造可怕的错误,尤其是如果他们不使用默认值(std::memory_order_seq_cst 内存排序)
2. 如果我是对的,这是否意味着此代码中的第二行是多余的:

atomicVar.store(42);
std::atomic_thread_fence(std::memory_order_seq_cst);  

3。从某种意义上说,std::atomic_thread_fences 是否与互斥锁具有相同的要求,以确保非原子变量的 seq 一致性必须执行 std::atomic_thread_fence(std::memory_order_seq_cst); 加载前和 std::atomic_thread_fence(std::memory_order_seq_cst);
在商店之后?
4.是

  {
    regularSum+=atomicVar.load();
    regularVar1++;
    regularVar2++;
    }
    //...
    {
    regularVar1++;
    regularVar2++;
    atomicVar.store(74656);
  }

相当于

std::mutex mtx;
{
   std::unique_lock<std::mutex> ul(mtx);
   sum+=nowRegularVar;
   regularVar++;
   regularVar2++;
}
//..
{
   std::unique_lock<std::mutex> ul(mtx);
    regularVar1++;
    regularVar2++;
    nowRegularVar=(74656);
}

我认为不是,但我想确定一下。

编辑: 5. 可以断言火吗?
只有两个线程存在。

atomic<int*> p=nullptr; 

第一个线程写入

{
    nonatomic_p=(int*) malloc(16*1024*sizeof(int));
    for(int i=0;i<16*1024;++i)
    nonatomic_p[i]=42;
    p=nonatomic;
}

第二个线程读取

{
    while (p==nullptr)
    {
    }
    assert(p[1234]==42);//1234-random idx in array
}

【问题讨论】:

    标签: c++ c++11 mutex atomic memory-barriers


    【解决方案1】:

    如果你喜欢处理栅栏,那么a.load(memory_order_acquire) 相当于a.load(memory_order_relaxed) 后跟atomic_thread_fence(memory_order_acquire)。同样,a.store(x,memory_order_release) 相当于在调用a.store(x,memory_order_relaxed) 之前调用atomic_thread_fence(memory_order_release)。 memory_order_consume 是 memory_order_acquire 的一个特例,仅用于相关数据。 memory_order_seq_cst 是特殊的,它构成了所有 memory_order_seq_cst 操作的总顺序。与其他混合,它与加载的获取和存储的释放相同。 memory_order_acq_rel用于读-修改-写操作,相当于RMW的读部分获取,写部分释放。

    对原子操作使用排序约束可能会也可能不会产生实际的栅栏指令,具体取决于硬件架构。在某些情况下,如果将排序约束放在原子操作上而不是使用单独的栅栏,编译器会生成更好的代码。

    在 x86 上,加载始终是获取的,而存储始终是释放的。 memory_order_seq_cst 需要使用MFENCE 指令或LOCK 前缀指令进行更强的排序(这里有一个实现选择,即是否使存储具有更强的排序或负载)。因此,独立的获取和释放栅栏是无操作的,但atomic_thread_fence(memory_order_seq_cst) 不是(再次需要MFENCE 或LOCKed 指令)。

    排序约束的一个重要影响是它们对其他操作进行排序。

    std::atomic<bool> ready(false);
    int i=0;
    
    void thread_1()
    {
        i=42;
        ready.store(true,memory_order_release);
    }
    
    void thread_2()
    {
        while(!ready.load(memory_order_acquire)) std::this_thread::yield();
        assert(i==42);
    }
    

    thread_2 旋转直到它从ready 读取true。由于在thread_1 中存储到ready 是一个释放,并且加载是一个获取,因此存储同步加载,并且存储到i 发生之前 来自断言中i 的加载,断言不会触发。

    2)

    中的第二行
    atomicVar.store(42);
    std::atomic_thread_fence(std::memory_order_seq_cst);  
    

    确实可能是多余的,因为atomicVar 的存储默认使用memory_order_seq_cst。但是,如果此线程上有其他非memory_order_seq_cst 原子操作,则栅栏可能会产生后果。例如,它将充当后续a.store(x,memory_order_relaxed) 的释放围栏。

    3) 栅栏和原子操作不像互斥锁那样工作。您可以使用它们来构建互斥锁,但它们不像它们那样工作。您不必使用atomic_thread_fence(memory_order_seq_cst)。不要求任何原子操作必须是memory_order_seq_cst,并且可以在没有原子变量的情况下实现对非原子变量的排序,如上例所示。

    4) 不,这些不相等。因此,没有互斥锁的 sn-p 是数据竞争和未定义的行为。

    5) 不,您的断言不能触发。使用 memory_order_seq_cst 的默认内存顺序,来自原子指针 p 的存储和加载就像我上面示例中的存储和加载一样工作,并且保证对数组元素的存储发生在读取之前。

    【讨论】:

    • so in 5) for(int i=0;i
    • 顺便说一句,您能否详细说明为什么 4) 是数据竞争?
    • 是的,你在 5 中是对的 --- 分配给 nonatomic_p[i] 的分配不能在分配 p 之后移动。
    • 4) 如果两个块在不同的线程上运行(我假设它们是这样,因此需要互斥锁),则属于数据竞争,因为没有什么可以命令写入 @987654357 @ 和 regularVar2。如果来自另一个线程的未同步的来自regularVar1 和regularVar2 的任何读取,这也是一场数据竞争。
    • a.load(mo_acquire) 比a.load(mo_relaxed); fence(mo_acquire) 弱,如果您考虑系统中的其他 宽松变量,对于不与a 同步的读者。 Preshing 解释 (preshing.com/20131125/…) 栅栏是 2 路屏障,但获取加载只是 1 路屏障:较早的加载可以重新排序超过 a.load(mo_acquire) 一直到关键部分或其他任何地方。但是负载栅栏会阻止 all 负载在任一方向上重新排序。所以你的前几句话并不完全准确。
    【解决方案2】:

    据我所知,std::memory_order_seq_cst 插入了内存屏障,而其他只强制对特定内存位置的操作进行排序。

    这真的取决于你在做什么,以及你在使用什么平台。与 IA64、PowerPC、ARM 等平台上的弱排序模型相比,x86 等平台上的强内存排序模型将对内存栅栏操作的存在提出不同的要求。std::memory_order_seq_cst 的默认参数是什么确保根据平台使用正确的内存栅栏指令。在像 x86 这样的平台上,不需要完整的内存屏障,除非您正在执行 read-modify-write 操作。根据 x86 内存模型,所有加载都具有加载获取语义,所有存储都具有存储释放语义。因此,在这些情况下,std::memory_order_seq_cst 枚举基本上创建了一个空操作,因为 x86 的内存模型已经确保这些类型的操作在线程之间是一致的,因此没有实现这些类型的部分内存屏障的汇编指令。因此,如果您在 x86 上显式设置 std::memory_order_release 或 std::memory_order_acquire 设置,则相同的无操作条件将成立。此外,在这些情况下需要完整的内存屏障将是不必要的性能障碍。如前所述,只有读取-修改-存储操作才需要它。

    但在其他内存一致性模型较弱的平台上,情况并非如此,因此使用std::memory_order_seq_cst 将采用适当的内存栅栏操作,而无需用户明确指定他们是否想要加载获取、存储-release,或完整的内存栅栏操作。这些平台具有执行此类内存一致性合同的特定机器指令,std::memory_order_seq_cst 设置将解决正确的情况。如果用户想专门调用这些操作之一,他们可以通过显式的 std::memory_order 枚举类型,但这不是必需的......编译器会计算出正确的设置。

    我想很多人会使用 std::atomic 制造可怕的错误,尤其是如果他们不使用默认值(std::memory_order_seq_cst 内存排序)

    是的,如果他们不知道自己在做什么,并且不了解在某些操作中需要哪些类型的内存屏障语义,那么如果他们试图显式地进行说明内存屏障的类型,它是不正确的,尤其是在不会帮助他们误解内存顺序的平台上,因为它们本质上较弱。

    最后,请记住关于互斥锁的情况#4,这里需要发生两件不同的事情:

    1. 不得允许编译器跨互斥锁和临界区重新排序操作(尤其是在优化编译器的情况下)
    2. 必须创建必要的内存栅栏(取决于平台),以维持所有存储在临界区和读取互斥变量之前完成的状态,并且所有存储在退出临界区之前完成。

    由于默认情况下,原子存储和加载是使用std::memory_order_seq_cst 实现的,因此使用原子也将实现适当的机制来满足条件#1 和#2。话虽如此,在您使用原子的第一个示例中,加载将强制块的获取语义,而存储将强制释放语义。虽然它不会在这两个操作之间的“关键部分”内强制执行任何特定的排序。在您的第二个示例中,您有两个带锁的不同部分,每个锁都具有获取语义。由于在某些时候您必须释放具有释放语义的锁,因此不,这两个代码块将不等效。在第一个示例中,您在加载和存储之间创建了一个很大的“关键部分”(假设这一切都发生在同一个线程上)。在第二个示例中,您有两个不同的关键部分。

    附:我发现以下 PDF 特别有启发性,您也可能会发现: http://www.nwcpp.org/Downloads/2008/Memory_Fences.pdf

    【讨论】:

    • 我认为(在你的 #2 中)从进入临界区之前的加载和存储可能会被移动到临界区,并且在 CS 之后的加载和存储可能会被移动到 CS。这意味着编译器仍然可以(在一定程度上)在 CS 边界周围重新排序非 CS 相关的加载和存储。这是因为这些加载/存储最初不是 CS 的一部分。但是什么都不能从 CS 之前移动到 CS 之后.. 只能移到它的中间。
    • 是的,这是正确的,至少我理解获取和释放语义的含义。
    • 是否可以在 C++11 中提供的原子操作套件中构建自己的工作互斥锁?
    • Yes and no ...您可以创建一个互斥锁,其中lock() 将是一个自旋锁(即忙等待操作),但您必须调用某种类型的基于操作系统的系统调用如果您要创建实际使用操作系统资源使线程进入睡眠状态的操作(即 Linux 上的 futex 等)。话虽这么说,如果你愿意,你可以使用原子来创建一个完整的用户级线程库......据我所知,这将是制作真正的用户级“互斥锁”的唯一方法" ...您仍然需要进行内核调用,但不一定是互斥锁本身...
    • @Jason - 因此,使用原子操作套件可以说“在此操作之前发生在代码中的所有写入都需要在此操作结果之前出现在内存中。”。换句话说,可以通知编译器某些重新排序无效。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-29
    • 1970-01-01
    • 1970-01-01
    • 2015-01-26
    • 2011-02-04
    • 1970-01-01
    相关资源
    最近更新 更多