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

时间:2011-09-18 12:26:23

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

我试着寻找这方面的细节,我甚至阅读了关于互斥和原子的标准......但我仍然无法理解C ++ 11内存模型的可见性保证。 据我所知,互斥BESIDE互斥的一个非常重要的特点是确保可见性。 Aka每次只有一个线程增加计数器是不够的,重要的是线程增加了最后使用互斥锁的线程存储的计数器(我真的不知道为什么人们在讨论时不再提这个互斥,也许我有坏老师:))。 因此,从我所说的原子不强制立即可见性: (来自维护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插入mem屏障,而其他只强制执行某些内存位置上的操作的顺序。

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

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

3。 do std :: atomic_thread_fences在某种意义上与互斥量具有相同的要求,为了确保非原子变量的seq一致性,必须执行std :: atomic_thread_fence(std :: memory_order_seq_cst); 在加载之前             的std :: atomic_thread_fence(标准:: 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 个答案:

答案 0 :(得分: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的特例,依赖数据memory_order_seq_cst很特殊,可以在所有memory_order_seq_cst次操作中形成总排序。与其他人混合,它与获取负载和商店的发布相同。 memory_order_acq_rel用于读 - 修改 - 写操作,相当于读取部分的获取和RMW写入部分的释放。

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

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

订购限制的一个重要影响是他们订购其他操作。

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旋转,直到它从true读取ready。由于ready中的thread_1商店是一个版本,而且加载是获取,因此商店加载同步,商店与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)这些不等同。因此,没有互斥锁的代码段就是数据争用和未定义的行为。

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

答案 1 :(得分:7)

  

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

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

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

  

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

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

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

  1. 不允许编译器对互斥锁和关键部分的操作重新排序(特别是在优化编译器的情况下)
  2. 必须创建必需的内存屏障(取决于平台),这些内存保持在关键部分之前完成所有存储并读取互斥锁变量的状态,并且所有存储在退出临界区之前完成。
  3. 因为默认情况下,原子存储和加载是用std::memory_order实现的,所以使用原子也会实现适当的机制来满足条件#1和#2。话虽如此,在您的第一个原子示例中,负载将强制执行块的获取语义,而存储将强制执行释放语义。但是,它不会强制执行这两个操作之间的“关键部分”内的任何特定顺序。在第二个示例中,您有两个带锁的不同部分,每个锁都具有获取语义。因为在某些时候你必须释放具有释放语义的锁,然后不,这两个代码块就不相同了。在第一个示例中,您在加载和存储之间创建了一个大的“关键部分”(假设这一切都发生在同一个线程上)。在第二个示例中,您有两个不同的关键部分。

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