在C11 / C ++ 11中,可以在同一个内存中混合原子/非原子操作吗?

时间:2016-01-31 01:43:59

标签: c++ c multithreading c++11 concurrency

是否可以在同一个内存位置执行原子操作和非原子操作?

我不是因为我实际上想要这样做,而是因为我正在尝试理解C11 / C ++ 11内存模型。他们定义了一个“数据竞争”,如下所示:

  

如果程序包含两个,则程序的执行包含数据竞争   不同线程中的冲突操作,其中至少有一个不是   原子,并没有发生在另一个之前。任何这样的数据竞争   导致未定义的行为。    - C11 §5.1.2.4p25, C ++ 11 §1.10p21

其中“至少有一个不是原子的”部分令我不安。如果不可能混合原子和非原子操作,它只会说“在一个非原子的对象上。”

我看不到对原子变量执行非原子操作的任何直接方法。 C ++中的std::atomic<T>没有定义任何非原子语义的操作。在C中,原子变量的所有直接读/写似乎都被转换为原子操作。

我认为memcpy()和其他直接内存操作可能是对原子变量执行非原子读/写的方式吗?即。 memcpy(&atomicvar, othermem, sizeof(atomicvar))?但这是否定义为行为?在C ++中,std::atomic是不可复制的,所以在C或C ++中它是memcpy()的定义行为吗?

原子变量的初始化(无论是通过构造函数还是atomic_init())被定义为不是原子的。但这是一次性操作:您不允许第二次初始化原子变量。放置新的或显式的析构函数调用也可能不是原子的。但是在所有这些情况下,似乎无论如何定义行为都不会导致可能在未初始化值上运行的并发原子操作。

对非原子变量执行原子操作似乎完全不可能:C和C ++都没有定义任何可以对非原子变量进行操作的原子函数。

那么这里的故事是什么?是真的关于memcpy(),还是初始化/破坏,还是别的什么?

2 个答案:

答案 0 :(得分:1)

我认为你忽略了另一个案例,相反的顺序。考虑初始化的int,其存储空间将被重用以创建std::atomic_int。所有原子操作都在ctor完成后发生,因此在初始化内存上发生。但是,对现在被覆盖的int的任何并发非原子访问也必须被禁止。

(我在这里假设存储寿命足够,不起作用)

我不完全确定,因为我认为第二次访问int无论如何都是无效的,因为访问表达式int的类型与当时对象的类型不匹配({ {1}})。但是,“对象的类型当时”假定单个线性时间进度在多线程环境中不成立。 C ++ 11通常通过对“ 全局状态”未定义行为本身做出这样的假设来解决,并且问题中的规则似乎适合该框架。

所以也许改写:如果一个内存位置包含一个原子对象以及一个非原子对象,并且在创建另一个(更新的)之前,对最早创建的(较旧的)对象的破坏没有按顺序排序对象,然后访问旧对象与访问较新对象冲突,除非前者被安排在后者之前。

答案 1 :(得分:0)

免责声明:我不是并行大师。

  

是否可以在同一内存中混合原子/非原子操作,如果   那怎么样?

你可以在代码中编写并编译,但它可能会产生未定义的行为。

在谈论原子论时,重要的是要了解他们解决了哪些问题。

正如您可能知道的那样,我们短暂称之为“记忆”的是多层实体,它们能够保存记忆。
首先我们有RAM,然后是缓存行,然后是寄存器。

在单核处理器上,我们没有任何同步问题。在多核处理器上我们拥有所有这些。每个核心都有自己的一组寄存器和缓存行。

这几乎没有问题。

其中第一个是内存重新排序 - CPU可以决定运行时来搜索一些读/写指令以使代码运行得更快。这可能会产生一些奇怪的结果,这些结果在带来这组指令的高级代码中完全不可见。
这个phenomanon最典型的例子是“两个线程 - 两个整数”的例子:

int i=0;
int j=0;
thread a -> i=1, then print j
thread b -> j=1 then print i;

逻辑上,结果“00”不能。或者首先结束,结果可以是“01”,或者b先结束,结果可以是“10”。如果它们都在同一时间结束,则结果可能是“11”。然而,如果你构建了一个小程序,它模仿这个位置并在一个循环中运行它,非常quicly你会看到结果“00”

另一个问题是记忆隐形。就像我之前提到的,变量的值可以缓存在其中一个缓存行中,或者存储在其中一个注册的缓存行中。当CPU更新变量值时 - 它可能会延迟将新值写回RAM。它可能会保留缓存/注册表中的值,因为它被告知(通过编译器优化)该值将很快再次更新,因此为了使程序更快 - 再次更新值,然后再将其写回内存。如果其他CPU(以及线程或进程)依赖于新值,则可能导致未定义的行为。

例如,看看这个伪代码:

bool b = true;
while (b) -> print 'a'
new thread -> sleep 4 seconds -> b=false;

字符“a”可以无限打印,因为b可能会被缓存而且永远不会更新。

在处理诉讼时,还有很多问题。

atomics解决了这些问题(简而言之)告诉编译器/ CPU如何正确地从RAM读取数据和从RAM写入数据而不做不想要的scrumble(读取内存命令 )。内存顺序可能会强制cpu将其值写回RAM,或者从RAM中读取值,即使它们被缓存。

所以,虽然你可以将非原子动作与原子动作混合,但你只做部分工作。

例如,让我们回到第二个例子:

atomic bool b = true;
while (reload b) print 'a'
new thread - > b = (non atomicly) false. 

所以尽管一个线程一次又一次地从RAM重新读取b的值,但另一个线程可能不会将false写回RAM。

因此,虽然您可以在代码中混合使用这些类型的操作,但它会产生不明确的行为。