在C11/C++11中,是否可以在同一内存中混合使用原子/非原子操作?

rseugnpd  于 2022-11-19  发布在  其他
关注(0)|答案(4)|浏览(201)

是否可以在同一内存位置上执行原子操作和非原子操作?
我这样问并不是因为我真的想这么做,而是因为我在试图理解C11/C11内存模型。
如果一个程序在不同的线程中包含两个冲突的动作,至少其中一个不是原子的,并且没有一个动作发生在另一个之前,那么这个程序的执行就包含了一个 * 数据竞争 *。任何这样的数据竞争都会导致未定义的行为。--C11§5.1.2.4 p25,**C
11**§ 1.10 p21
“至少有一个不是原子的”这一部分让我感到困扰。如果不能混合原子和非原子操作,它只会说“在一个不是原子的对象上”。
我看不出有什么简单的方法可以在原子变量上执行非原子操作。C中的std::atomic<T>没有定义任何具有非原子语义的操作。在C中,所有对原子变量的直接读/写似乎都被转换为原子操作。
我想memcpy()和其他直接内存操作可能是对原子变量执行非原子读/写的一种方式?即memcpy(&atomicvar, othermem, sizeof(atomicvar))?但这是定义的行为吗?在C
中,std::atomic是不可复制的,所以它是C或Cmemcpy()的定义行为吗?
原子变量的初始化(无论是通过构造函数还是atomic_init())被定义为非原子的。但这是一次性操作:你不允许第二次初始化原子变量。新的放置或者显式析构函数调用也可能不是原子的。但是在所有这些情况下,似乎无论如何都不会定义并发原子操作对未初始化的值进行操作。
对非原子变量执行原子操作似乎完全不可能:C和C
都没有定义任何可以操作非原子变量的原子函数。
那么这里的故事是什么呢?它真的是关于memcpy(),或者初始化/销毁,或者其他什么?

vatpfxk5

vatpfxk51#

我想你忽略了另一种情况,即相反的顺序。考虑一个初始化的int,它的存储被重用来创建一个std::atomic_int。所有的原子操作都发生在它的ctor完成之后,因此也发生在初始化的内存上。但是,任何对现在被覆盖的int的并发、非原子访问也必须被禁止。
(这里我假设存储寿命足够长,不起任何作用)
我不能完全确定,因为我认为对int的第二次访问无论如何都是无效的,因为访问表达式int的类型与当时对象的类型不匹配然而,“对象的类型在时间”假设一个单一的线性时间进程,这在多线程环境中不成立。C++11一般来说,通过对“**全球状态”未定义行为本身做出这样的假设来解决这个问题,来自这个问题的规则似乎适合这个框架。
所以也许换句话说:如果单个存储器位置包含原子对象以及非原子对象,并且如果最早创建的(较旧的)对象的破坏不是按顺序的-在创建另一个(较新的)对象之前,则对较旧的对象的访问与对较新的对象的访问冲突,除非前者被安排在后者之前。

vxf3dgd4

vxf3dgd42#

免责声明:我不是一个并行Maven。
是否可以在同一内存中混合原子/非原子操作?如果可以,如何混合?
您可以在代码中编写它并进行编译,但它可能会产生未定义的行为。
当谈论原子时,重要的是要理解它们解决什么样的问题。
正如你可能知道的,我们简单地称之为“内存”是能够容纳内存的多层实体集。
首先是RAM,然后该高速缓存线,最后是寄存器。
在单核处理器上,我们没有任何同步问题。在多核处理器上,我们有所有这些问题。每个内核都有自己的一组寄存器和高速缓存线。
这几乎不会产生问题。
第一个是内存重新排序-CPU可能会在运行时决定压缩一些阅读/写指令,以使代码运行得更快。这可能会产生一些奇怪的结果,这些结果在带来这组指令的高级代码上完全不可见。
这种现象最经典的例子是“两个线程-两个整数”的例子:

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

从逻辑上讲,结果“00”不可能是“01”,如果a先结束,结果可能是“01”,如果b先结束,结果可能是“10”,如果两者同时结束,结果可能是“11”,但是,如果你建立一个模仿这种情况的小程序,并在循环中运行它,你很快就会看到结果“00”。
另一个问题是内存不可见性。就像我前面提到的,变量的值可能被缓存在该高速缓存行中,或者被存储在一个寄存器中。当CPU更新变量值时-它可能会延迟将新值写回RAM。它可能会将值保存在缓存/寄存器中,因为它被告知(通过编译器优化)该值将很快被再次更新,S7 - 1200可编程控制器(以及线程或进程)依赖于新值。
例如,请看下面伪代码:

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

因为b可以被高速缓存并且从不被更新,所以字符“a”可以被无限地打印。
在处理平行语时还有许多问题。
原子通过(简单地说)告诉编译器/CPU如何正确地从/向RAM读取和写入数据,而不进行不必要的滚动(读取有关 * 内存顺序 *),解决了这类问题。内存顺序可能会强制CPU将其值写回RAM,或从RAM读取值,即使这些值已被缓存。
因此,尽管你可以将非原子操作与原子操作混合,但你只是完成了部分工作。
例如,让我们回到第二个示例:

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

因此,尽管一个线程一次又一次地从RAM重新读取B的值,但是另一个线程可能不将false写回到RAM。
因此,尽管您可以在代码中混合使用这类操作,但这会产生不完善的行为。

zpgglvta

zpgglvta3#

我对这个主题很感兴趣,因为我的代码有时需要串行访问一系列地址,有时需要通过某种管理争用的方式并行访问相同的地址。
因此,这并不完全是最初的问题所提出的情况,(我认为)它意味着并行代码中的原子和非原子操作 * 并发 *,或者几乎如此,但很接近。
我通过一些不正当的强制转换说服了我的C11编译器允许我原子地和非原子地(“直接”)访问整数,更有用的是允许我原子地和非原子地(“直接”)访问指针,我已经确定这两种类型在我的x86_64系统上都是无锁的。也就是说,原子类型和非原子类型的大小是相同的。
我绝对不会尝试在并行环境中混合两种类型的地址访问,那注定会失败。然而,我已经成功地在串行代码中使用了“直接”语法操作,在并行代码中使用了“原子”语法,这让我在串行环境中获得了最快的访问速度(和更简单的语法),而在并行环境中安全地管理了争用。
因此,只要不试图在并行代码中混合使用这两种方法,并且坚持使用无锁类型,这可能意味着最大为指针大小,就可以做到这一点。

b1zrtrql

b1zrtrql4#

我对这个主题很感兴趣,因为我的代码有时需要串行访问一系列地址,有时需要通过某种管理争用的方式并行访问相同的地址。
因此,与最初的问题所提出的情况(我认为)并不完全相同,它意味着并行代码中的原子和非原子操作是 * 并发 * 的,或者几乎是这样,但很接近。
我通过一些迂回的强制转换来说服我的C11编译器允许我访问整数,更有用的是允许我原子地和非原子地访问指针(“directly”),因为这两种类型在我的x86_64系统上都是正式的无锁的。对此的解释是原子和非原子类型的大小是相同的,并且硬件可以在单个操作中更新这些类型。
我绝对不会尝试在并行环境中混合两种类型的地址访问,我认为这注定会失败。然而,我已经成功地在串行代码中使用了“直接”语法操作,在并行代码中使用了“原子”语法,这让我在串行环境中获得了最快的访问速度(和更简单的语法),而在并行环境中安全地管理了争用。
因此,只要不试图在并行代码中混合使用这两种方法,并且坚持使用无锁类型,这可能意味着最大为指针大小,就可以做到这一点。

相关问题