首页 文章

内存模型的排序和可见性?

提问于
浏览
26

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

具有memory_order_seq_cst的fence不会强制立即查看其他线程(并且MFENCE指令也不会) . C 0x内存排序约束只是---排序约束 . memory_order_seq_cst操作形成一个总顺序,但对该顺序没有任何限制,除了它必须由所有线程达成一致,并且它不得违反其他排序约束 . 特别是,如果线程按照与约束一致的顺序看到值,则线程可能会在一段时间内继续看到“陈旧”值 .

而且我很好 . 但问题是我无法理解关于原子的C 11构造是什么"global"并且只能确保原子变量的一致性 . 特别是我了解以下内存排序中的哪些(如果有)保证在加载和存储之前和之后将有一个内存栅栏:http://www.stdthread.co.uk/doc/headers/atomic/memory_order.html

从我可以告诉std :: memory_order_seq_cst插入mem屏障,而其他只强制执行某些内存位置上的操作的顺序 .

那么有人可以清楚这一点,我认为很多人会使用std :: atomic制作可怕的错误,esp如果他们不使用默认值(std :: memory_order_seq_cst内存排序)
2.如果我是对的,这意味着第二行在此代码中是冗余的:

atomicVar.store(42);
std::atomic_thread_fence(std::memory_order_seq_cst);
  1. std :: atomic_thread_fences在某种意义上与互斥量具有相同的要求,为了确保非原子变量的seq一致性,必须执行std :: atomic_thread_fence(std :: memory_order_seq_cst);在load和std :: atomic_thread_fence(std :: memory_order_seq_cst)之前;
    经过商店?
    是的
{
    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
}

2 回答

  • 24

    如果你喜欢处理围栏,那么 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_consumememory_order_acquire 的特例,用于从属数据 only . memory_order_seq_cst 是特殊的,并且在所有 memory_order_seq_cst 操作中形成总订单 . 与其他人混合,它与获取负载和商店的发布相同 . memory_order_acq_rel 用于读取 - 修改 - 写入操作,相当于读取部分上的获取和RMW写入部分上的释放 .

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

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

    排序约束的一个重要影响是它们命令 other 操作 .

    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 加载之前发生商店_1161988_,并且断言将不会触发 .

    2)第二行

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

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

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

    4)这些不等同 . 因此,没有互斥锁的代码段是数据争用和未定义的行为 .

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

  • 7

    从我可以告诉std :: memory_order_seq_cst插入mem屏障,而其他只强制执行某些内存位置上的操作的顺序 .

    这真的取决于你're doing, and on what platform you'正在使用什么 . 与像IA64,PowerPC,ARM等平台上的较弱排序模型相比,像x86这样的平台上强大的内存排序模型将为内存栅栏操作的存在创建一组不同的要求. std::memory_order_seq_cst 的默认参数是什么?根据平台,将使用适当的内存栅栏指令 . 在像x86这样的平台上,除非您正在执行读 - 修改 - 写操作,否则不需要完整的内存屏障 . 根据x86内存模型,所有加载都具有加载获取语义,并且所有商店都具有存储释放语义 . 因此,在这些情况下, std::memory_order_seq_cst 枚举基本上创建了一个无操作,因为x86的内存模型已经确保这些类型的操作在线程之间是一致的,因此没有实现这些类型的部分内存屏障的汇编指令 . 因此,如果在x86上明确设置 std::memory_order_releasestd::memory_order_acquire 设置,则相同的无操作条件将成立 . 此外,在这些情况下需要完全的存储器屏障将是不必要的性能障碍 . 如上所述,只需要读取 - 修改 - 存储操作 .

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

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

    是的,如果他们不知道他们正在做什么,并且不了解在某些操作中要求哪种类型的内存屏障语义,那么如果他们试图明确说明类型会有很多错误记忆障碍,这是不正确的,特别是在平台上,由于它们本质上较弱,不会帮助他们对记忆排序的误解 .

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

    • 不允许编译器对互斥锁和关键部分的操作重新排序(特别是在优化编译器的情况下)

    • 必须创建必要的内存屏障(取决于平台),这些内存保持在关键部分之前完成所有存储并读取互斥变量的状态,并且所有存储在退出临界区之前完成 .

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

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

相关问题