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

NoS*_*tAl 27 c++ mutex atomic memory-barriers c++11

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

具有memory_order_seq_cst的fence不会强制立即查看其他线程(并且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插入mem屏障,而其他只强制执行某些内存位置上的操作的顺序.

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

atomicVar.store(42);
std::atomic_thread_fence(std::memory_order_seq_cst);  
Run Code Online (Sandbox Code Playgroud)

3. 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);
  }
Run Code Online (Sandbox Code Playgroud)

相当于

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);
}
Run Code Online (Sandbox Code Playgroud)

我想不是,但我想确定.

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

atomic<int*> p=nullptr; 
Run Code Online (Sandbox Code Playgroud)

第一个线程写

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

第二个线程读取

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

Ant*_*ams 25

如果你喜欢处理围栏,那么a.load(memory_order_acquire)相当于a.load(memory_order_relaxed)后面跟着atomic_thread_fence(memory_order_acquire).同样,a.store(x,memory_order_release)相当于调用atomic_thread_fence(memory_order_release)之前的调用a.store(x,memory_order_relaxed).memory_order_consumememory_order_acquire针对依赖数据的特例.很特别,并且形成了所有操作的总订单.与其他人混合,它与获取负载和商店的发布相同.用于读 - 修改 - 写操作,相当于读取部分的获取和RMW写入部分的释放.memory_order_seq_cstmemory_order_seq_cstmemory_order_acq_rel

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

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

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

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);
}
Run Code Online (Sandbox Code Playgroud)

thread_2旋转,直到它读取true距离ready.由于存储到readythread_1是一个版本,并且负载是一个获取,然后存储负载同步,并且存储在断言中加载i 之前发生i,并且断言不会触发.

2)第二行

atomicVar.store(42);
std::atomic_thread_fence(std::memory_order_seq_cst);  
Run Code Online (Sandbox Code Playgroud)

确实可能是多余的,因为商店默认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)这些不等同.因此,没有互斥锁的代码段是数据争用和未定义的行为.

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

  • `a.load(mo_acquire)` 比 `a.load(mo_relaxed) 弱;fence(mo_acquire)`,如果你考虑系统中的 *other* 宽松变量,对于不与 `a` 同步的读者。Preshing 解释说 (http://preshing.com/20131125/acquire-and-release-fences-dont-work-the-way-youd-expect/) 围栏是双向障碍,但获取负载只是一个单向屏障:较早的加载可以重新排序越过`a.load(mo_acquire)` 一直到临界区或其他任何地方。但是负载栅栏会阻止*所有*负载在它的任一方向上重新排序。所以你的前几句话并不完全准确。 (2认同)

Jas*_*son 7

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

这取决于你正在做什么,以及你正在使用什么平台.与像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将使用适当的内存栅栏操作,而无需用户必须明确指定是否需要加载,存储释放或完整内存围栏操作.这些平台具有用于强制执行此类内存一致性合同的特定计算机指令,并且该std::memory_order_seq_cst设置将确定正确的情况.如果用户想要专门调用其中一个操作,他们可以通过显式std::memory_order枚举类型,但是没有必要......编译器会计算出正确的设置.

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

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

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

  1. 不得允许编译器对互斥锁和关键部分的操作重新排序(特别是在优化编译器的情况下)
  2. 必须创建必要的内存屏障(取决于平台),这些内存保持所有存储在临界区之前完成并读取互斥变量的状态,并且所有存储在退出临界区之前完成.

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

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