27

我试着寻找这方面的细节,我什至阅读了关于互斥锁和原子的标准......但我仍然无法理解 C++11 内存模型可见性保证。据我了解,互斥互斥的非常重要的特性是确保可见性。也就是每次只有一个线程增加计数器是不够的,重要的是线程增加最后使用互斥锁的线程存储的计数器(我真的不知道为什么人们在讨论时不提这个互斥体,也许我有不好的老师:))。因此,据我所知, 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
}
4

2 回答 2

28

如果你喜欢处理栅栏,那么a.load(memory_order_acquire)就相当于a.load(memory_order_relaxed)后面跟着atomic_thread_fence(memory_order_acquire)。类似地,a.store(x,memory_order_release)相当于在调用 toatomic_thread_fence(memory_order_release)之前调用 to a.store(x,memory_order_relaxed)memory_order_consume是 , 的特例,memory_order_acquire适用于从属数据。是特殊的,并在所有操作中形成一个总顺序。与其他混合,它与加载的获取和存储的释放相同。用于read-modify-write操作,相当于RMW的read部分的acquire和write部分的release。memory_order_seq_cstmemory_order_seq_cstmemory_order_acq_rel

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

在 x86 上,加载始终是获取的,而存储始终是释放的。memory_order_seq_cst需要使用MFENCE指令或LOCK前缀指令进行更强的排序(这里有一个实现选择,即是否使存储具有更强的排序或负载)。因此,独立的获取和释放栅栏是无操作的,但atomic_thread_fence(memory_order_seq_cst)不是(再次需要MFENCEor 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旋转直到它从true读取ready。由于存储到readyinthread_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的存储和加载就像我上面示例中的存储和加载一样工作,并且保证对数组元素的存储发生在读取之前。

于 2011-10-19T16:54:50.807 回答
7

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

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

但在其他内存一致性模型较弱的平台上,情况并非如此,因此 usingstd::memory_order_seq_cst将采用适当的内存栅栏操作,而无需用户明确指定他们想要加载-获取、存储-释放还是完整内存围栏操作。这些平台具有执行此类内存一致性合同的特定机器指令,并且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。话虽如此,在您使用原子的第一个示例中,加载将强制块的获取语义,而存储将强制释放语义。虽然它不会在这两个操作之间的“关键部分”内强制执行任何特定的排序。在您的第二个示例中,您有两个带锁的不同部分,每个锁都具有获取语义。由于在某些时候您必须释放具有释放语义的锁,因此不,这两个代码块将不等效。在第一个示例中,您在加载和存储之间创建了一个很大的“关键部分”(假设这一切都发生在同一个线程上)。在第二个示例中,您有两个不同的关键部分。

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

于 2011-09-18T17:18:55.243 回答