num ++是'int num'的原子吗?

时间:2016-09-08 14:39:52

标签: c++ c multithreading assembly atomic

通常,对于int numnum++(或++num),作为读取 - 修改 - 写入操作,非原子。但我经常看到编译器,例如GCC,为它生成以下代码(try here):

Enter image description here

由于与num++对应的第5行是一条指令,我们可以得出结论:在这种情况下num++ 是原子的吗?

如果是这样,那么是否意味着如此生成的num++可以在并发(多线程)场景中使用,而不会有数据争用的危险(即我们不需要例如,std::atomic<int>,并强加相关的成本,因为它是原子的吗?

更新

请注意,这个问题不是是否增量原子(它不是,那是,并且是问题的开头行)。它是否可以处于特定场景中,即在某些情况下是否可以利用单指令性质来避免lock前缀的开销。并且,正如接受的答案在关于单处理器机器以及this answer的部分中提到的,其评论和其他人的对话解释,它可以(尽管不是C或C ++)。

12 个答案:

答案 0 :(得分:177)

这绝对是C ++定义为导致未定义行为的数据争用的原因,即使一个编译器碰巧生成了在某些目标计算机上执行了您所希望的代码。您需要使用std::atomic来获得可靠的结果,但如果您不关心重新排序,则可以将其与memory_order_relaxed一起使用。请参阅下面的示例代码和使用fetch_add的asm输出。

但首先,汇编语言是问题的一部分:

  

由于num ++是一条指令(add dword [num], 1),我们可以得出结论,在这种情况下num ++是原子的吗?

内存目标指令(纯存储除外)是在多个内部步骤中发生的读取 - 修改 - 写入操作。没有修改架构寄存器,但CPU必须在内部保存数据,同时通过其ALU发送数据。即使是最简单的CPU,实际寄存器文件也只是数据存储的一小部分,锁存器将一级输出作为另一级的输入等等。

来自其他CPU的内存操作可以在加载和存储之间全局可见。即在循环中运行add dword [num], 1的两个线程将踩到彼此的商店。 (参见@Margaret's answer获得一个漂亮的图表)。从两个线程中的每个线程增加40k后,计数器可能仅在实际的多核x86硬件上增加了大约60k(而不是80k)。

&#34; Atomic&#34;,来自希腊词,意思是不可分割的,意味着没有观察者可以操作视为单独的步骤。同时发生所有位的物理/电气瞬间发生只是实现负载或存储的一种方法,但对于ALU操作来说甚至都不可能。我详细介绍了有关纯负载的信息。我对 Atomicity on x86 的回答中的纯商店,而这个回答主要集中在读 - 修改 - 写。

lock prefix可以应用于许多读 - 修改 - 写(内存目标)指令,使整个操作相对于系统中所有可能的观察者都是原子的(其他核心和DMA设备,而不是示波器挂钩)直到CPU引脚)。这就是它存在的原因。 (另见this Q&A)。

所以lock add dword [num], 1 原子。运行该指令的CPU内核将使高速缓存行在其私有L1高速缓存中保持固定状态,从加载时从高速缓存读取数据,直到存储将其结果提交回高速缓存。根据{{​​3}}(或多核AMD使用的MOESI / MESIF版本)的规则,这可以防止系统中的任何其他缓存在从加载到存储的任何点都拥有缓存行的副本。 / Intel CPUs,分别)。因此,其他核心的操作似乎发生在之前或之后,而不是在此期间。

如果没有lock前缀,另一个核心可以获取缓存行的所有权并在我们加载之后但在我们的商店之前修改它,以便其他商店在我们的加载和存储之间变得全局可见。其他几个答案都是错误的,并声称如果没有lock,您将获得相同缓存行的冲突副本。在具有连贯缓存的系统中永远不会发生这种情况。

(如果lock ed指令对跨越两个缓存行的内存进行操作,那么确保对象的两个部分的更改在传播给所有观察者时保持原子性需要做更多的工作,所以没有观察者可以看到撕裂.CPU可能必须锁定整个内存总线,直到数据到达内存。不要使你的原子变量错位!)

请注意,lock前缀也会将指令转换为完整的内存屏障(如MESI cache coherency protocol),停止所有运行时重新排序,从而提供顺序一致性。 (请参阅MFENCE。他的其他帖子也非常出色,并清楚地解释了关于Jeff Preshing's excellent blog post的好东西的很多,从x86和其他硬件细节到C ++规则。)

在单处理器计算机上,或在单线程进程中,单个lock-free programming指令实际 原子,没有lock前缀。其他代码访问共享变量的唯一方法是让CPU执行上下文切换,这不能在指令中间发生。因此,普通dec dword [num]可以在单线程程序与其信号处理程序之间或在单核机器上运行的多线程程序之间进行同步。请参阅RMW及其下的评论,我会在其中详细解释。

回到C ++:

使用num++而不告诉编译器需要将其编译为单个读 - 修改 - 写实现完全是假的:

;; Valid compiler output for num++
mov   eax, [num]
inc   eax
mov   [num], eax

如果稍后使用num的值,则很可能:编译器会在增量后将其保留在寄存器中。因此,即使您检查num++如何自行编译,更改周围的代码也会影响它。

(如果稍后不需要该值,则inc dword [num]是首选;现代x86 CPU将至少与使用三个单独的指令一样有效地运行内存目标RMW指令。有趣的事实:{{3因为(奔腾)P5的超标量流水线并没有按照P6和后来的微体系结构的方式将复杂指令解码为多个简单的微操作。有关详细信息,请参阅the second half of my answer on another question,{ {3}}标记wiki以获取许多有用的链接(包括英特尔的x86 ISA手册,可以PDF格式免费获得)。

不要将目标内存模型(x86)与C ++内存模型混淆

允许

gcc -O3 -m32 -mtune=i586 will actually emit this 。使用std :: atomic获得的另一部分是对编译时重新排序的控制,以确保只有在执行其他操作后,num++才会全局可见。

经典示例:将一些数据存储到缓冲区中以供另一个线程查看,然后设置一个标志。即使x86确实免费获得加载/发布存储,您仍然必须告诉编译器不要使用flag.store(1, std::memory_order_release);重新排序。

您可能希望此代码与其他线程同步:

// flag is just a plain int global, not std::atomic<int>.
flag--;       // This isn't a real lock, but pretend it's somehow meaningful.
modify_a_data_structure(&foo);    // doesn't look at flag, and the compilers knows this.  (Assume it can see the function def).  Otherwise the usual don't-break-single-threaded-code rules come into play!
flag++;

但它没有赢。编译器可以在函数调用中自由移动flag++(如果它内联函数或者知道它没有查看flag)。然后它可以完全优化掉修改,因为flag不是volatile。 (不,C ++ volatile不是std :: atomic的有用替代品.std :: atomic确实让编译器假设内存中的值可以异步修改,类似于volatile,但是&#39}除此之外,volatile std::atomic<int> foostd::atomic<int> foo不同,正如@Richard Hodges所讨论的那样。)

定义非原子变量上的数据竞争,因为未定义行为使编译器仍然可以提升负载并将存储从循环中提取出来,以及许多其他多线程可能引用的内存优化。 (有关UB如何启用编译器优化的更多信息,请参阅Agner Fog's instruction tables / microarchitecture guide。)

正如我所提到的,是一个完整的内存屏障,因此使用num.fetch_add(1, std::memory_order_relaxed);在x86上生成与num++相同的代码(默认为顺序一致性),但它可以是在其他架构(如ARM)上效率更高。即使在x86上,relax也允许更多的编译时重新排序。

这是GCC在x86上实际执行的操作,适用于对std::atomic全局变量进行操作的一些函数。

Compile-time reordering上查看格式良好的源代码+汇编语言代码。您可以选择其他目标体系结构,包括ARM,MIPS和PowerPC,以查看您从原子中获得的那些目标体系结构语言代码。

#include <atomic>
std::atomic<int> num;
void inc_relaxed() {
  num.fetch_add(1, std::memory_order_relaxed);
}

int load_num() { return num; }            // Even seq_cst loads are free on x86
void store_num(int val){ num = val; }
void store_num_release(int val){
  num.store(val, std::memory_order_release);
}
// Can the compiler collapse multiple atomic operations into one? No, it can't.

# g++ 6.2 -O3, targeting x86-64 System V calling convention. (First argument in edi/rdi)
inc_relaxed():
    lock add        DWORD PTR num[rip], 1      #### Even relaxed RMWs need a lock. There's no way to request just a single-instruction RMW with no lock, for synchronizing between a program and signal handler for example. :/ There is atomic_signal_fence for ordering, but nothing for RMW.
    ret
inc_seq_cst():
    lock add        DWORD PTR num[rip], 1
    ret
load_num():
    mov     eax, DWORD PTR num[rip]
    ret
store_num(int):
    mov     DWORD PTR num[rip], edi
    mfence                          ##### seq_cst stores need an mfence
    ret
store_num_release(int):
    mov     DWORD PTR num[rip], edi
    ret                             ##### Release and weaker doesn't.
store_num_relaxed(int):
    mov     DWORD PTR num[rip], edi
    ret

注意顺序一致性存储后如何需要MFENCE(完全屏障)。 x86通常是强烈排序的,但允许StoreLoad重新排序。拥有存储缓冲区对于流水线无序CPU的良好性能至关重要。 Jeff Preshing的 this LLVM blog 显示了使用MFENCE的后果,真实代码显示了在真实硬件上发生的重新排序。

回复:关于@Richard Hodges&#39;的评论中的讨论回答编译器将std :: atomic num++; num-=2;操作合并为一个num--;指令

关于同一主题的单独问答: x86 lock prefix ,我的回答重申了我在下面写的很多内容。

目前的编制者实际上并没有这样做,但并不是因为他们不允许这样做。 Godbolt compiler explorer 讨论了许多程序员对编译器不会做出令人惊讶的期望&#34;优化,以及标准可以做什么来让程序员控制。 Memory Reordering Caught in the Act讨论了许多可以优化的事例,包括这个例子。它指出内联和常量传播可以引入像fetch_or(0)这样的东西,它们可能只能变成load()(但仍然具有获取和释放语义),即使原始来源没有&#39} ; t有任何明显多余的原子操作。

编译器尚未执行的真正原因是:(1)没有人编写复杂的代码,允许编译器安全地执行此操作(不会出错),( 2)它可能违反Why don't compilers merge redundant std::atomic writes?。无锁代码很难在一开始就正确编写。因此,在使用原子武器时不要随便:它们不便宜而且不会优化太多。但是,使用std::shared_ptr<T>避免冗余原子操作并不容易,因为它没有非原子版本(尽管C++ WG21/P0062R1: When should compilers optimize atomics?提供了一种简单的方法来定义gcc的shared_ptr_unsynchronized<T>

回到num++; num-=2;进行编译,好像它是num--: 除非numvolatile std::atomic<int>,否则允许编译执行此操作。如果可以重新排序,则as-if规则允许编译器在编译时决定始终以这种方式发生。没有什么能保证观察者能够看到中间值(num++结果)。

即。如果在这些操作之间没有任何变得全局可见的排序与源的排序要求兼容  (根据抽象机器的C ++规则,而不是目标体系结构),编译器可以发出单个lock dec dword [num]而不是lock inc dword [num] / lock sub dword [num], 2

num++; num--无法消失,因为它与查看num的其他线程仍然具有“同步”关系,并且它既是获取加载也是释放 - 不允许在此主题中重新排序其他操作的商店。对于x86,这可能能够编译为MFENCE,而不是lock add dword [num], 0(即num += 0)。

N4455中所讨论的,在编译时更积极地合并非相邻原子操作可能是错误的(例如,进度计数器仅在结束时更新一次而不是每次迭代),但它也可以帮助没有缺点的性能(例如,如果编译器可以证明在临时的整个生命周期中存在另一个shared_ptr对象,则在创建和销毁shared_ptr的副本时跳过ref的原子inc / dec计数。 )

当一个线程立即解锁并重新锁定时,即使num++; num--合并也可能会损害锁定实现的公平性。如果它从未在asm中实际发布过,那么即使是硬件仲裁机制也不会让另一个线程有机会在那时抓住锁。

使用当前的gcc6.2和clang3.9,在最明显可优化的情况下,即使使用lock,您仍然会获得单独的memory_order_relaxed ed操作。 (principle of least surprise以便您可以查看最新版本是否不同。)

void multiple_ops_relaxed(std::atomic<unsigned int>& num) {
  num.fetch_add( 1, std::memory_order_relaxed);
  num.fetch_add(-1, std::memory_order_relaxed);
  num.fetch_add( 6, std::memory_order_relaxed);
  num.fetch_add(-5, std::memory_order_relaxed);
  //num.fetch_add(-1, std::memory_order_relaxed);
}

multiple_ops_relaxed(std::atomic<unsigned int>&):
    lock add        DWORD PTR [rdi], 1
    lock sub        DWORD PTR [rdi], 1
    lock add        DWORD PTR [rdi], 6
    lock sub        DWORD PTR [rdi], 5
    ret

答案 1 :(得分:39)

...现在让我们启用优化:

f():
        rep ret

好的,让我们给它一个机会:

void f(int& num)
{
  num = 0;
  num++;
  --num;
  num += 6;
  num -=5;
  --num;
}

结果:

f(int&):
        mov     DWORD PTR [rdi], 0
        ret

另一个观察线程(甚至忽略缓存同步延迟)没有机会观察到个别变化。

比较:

#include <atomic>

void f(std::atomic<int>& num)
{
  num = 0;
  num++;
  --num;
  num += 6;
  num -=5;
  --num;
}

结果是:

f(std::atomic<int>&):
        mov     DWORD PTR [rdi], 0
        mfence
        lock add        DWORD PTR [rdi], 1
        lock sub        DWORD PTR [rdi], 1
        lock add        DWORD PTR [rdi], 6
        lock sub        DWORD PTR [rdi], 5
        lock sub        DWORD PTR [rdi], 1
        ret

现在,每项修改都是: -

  1. 在另一个帖子中可观察到,
  2. 尊重在其他线程中发生的类似修改。
  3. 原子性不只是在指令级别,它涉及从处理器,缓存到内存和返回的整个流水线。

    更多信息

    关于优化std::atomic s。

    更新的效果

    c ++标准有&#39;好像&#39;规则,允许编译器重新排序代码,甚至重写代码,前提是结果具有完全相同的可观察效果(包括副作用),就好像它只是简单地执行了代码一样。

    as-if规则是保守的,尤其涉及原子。

    考虑:

    void incdec(int& num) {
        ++num;
        --num;
    }
    

    因为没有互斥锁,原子或任何其他影响线程间排序的结构,我认为编译器可以自由地将此函数重写为NOP,例如:

    void incdec(int&) {
        // nada
    }
    

    这是因为在c ++内存模型中,没有其他线程可以观察到增量的结果。如果numvolatile(可能会影响硬件行为),那当然会有所不同。但在这种情况下,此函数将是修改此内存的唯一函数(否则程序格式不正确)。

    然而,这是一场不同的球赛:

    void incdec(std::atomic<int>& num) {
        ++num;
        --num;
    }
    

    num是一个原子。对它的更改必须可以观察到正在观看的其他线程。更改这些线程本身(例如在增量和减量之间将值设置为100)将对num的最终值产生非常深远的影响。

    这是一个演示:

    #include <thread>
    #include <atomic>
    
    int main()
    {
        for (int iter = 0 ; iter < 20 ; ++iter)
        {
            std::atomic<int> num = { 0 };
            std::thread t1([&] {
                for (int i = 0 ; i < 10000000 ; ++i)
                {
                    ++num;
                    --num;
                }
            });
            std::thread t2([&] {
                for (int i = 0 ; i < 10000000 ; ++i)
                {
                    num = 100;
                }
            });
    
            t2.join();
            t1.join();
            std::cout << num << std::endl;
        }
    }
    

    示例输出:

    99
    99
    99
    99
    99
    100
    99
    99
    100
    100
    100
    100
    99
    99
    100
    99
    99
    100
    100
    99
    

答案 2 :(得分:36)

没有很多复杂情况,像add DWORD PTR [rbp-4], 1这样的指令非常具有CISC风格。

执行三个操作:从内存加载操作数,递增操作数,将操作数存储回内存 在这些操作期间,CPU获取并释放总线两次,在任何其他代理之间也可以获取它,这违反了原子性。

AGENT 1          AGENT 2

load X              
inc C
                 load X
                 inc C
                 store X
store X

X仅增加一次。

答案 3 :(得分:11)

添加指令原子。它引用内存,两个处理器内核可能具有该内存的不同本地缓存。

IIRC add指令的原子变体名为 lock xadd

答案 4 :(得分:10)

  

由于对应于num ++的第5行是一条指令,我们可以得出结论,在这种情况下num ++是原子的吗?

基于&#34;逆向工程&#34;得出结论是危险的。生成组件。例如,您似乎已经编译了禁用优化的代码,否则编译器会抛弃该变量或直接将1加载到它而不调用operator++。因为生成的程序集可能会根据优化标记,目标CPU等发生显着变化,所以您的结论基于沙子。

另外,你认为一个汇编指令意味着一个操作是原子的也是错误的。即使在x86架构上,这个add在多CPU系统上也不是原子的。

答案 5 :(得分:9)

在单核x86机器上,add指令相对于CPU 1 上的其他代码通常是原子的。中断不能在中间分割单个指令。

需要乱序执行以保持在单个内核中按顺序一次执行一条指令的错觉,因此在同一CPU上运行的任何指令都将在添加之前或之后完全发生。

现代x86系统是多核的,因此单处​​理器特殊情况并不适用。

如果一个人瞄准的是小型嵌入式PC并且没有计划将代码转移到其他任何东西,那么&#34;添加&#34;教学可以被利用。另一方面,操作固有原子的平台变得越来越稀缺。

(如果您正在使用C ++编写,这对您没有帮助。编译器没有选择要求num++编译到内存目标add或xadd 没有 lock前缀。他们可以选择将num加载到寄存器中并使用单独的指令存储增量结果,如果使用结果,可能会这样做。 )

脚注1:即使在原始8086上也存在lock前缀,因为I / O设备与CPU同时运行;单核系统上的驱动程序需要lock add以原子方式增加设备内存中的值,如果设备也可以修改它,或者关于DMA访问。

答案 6 :(得分:9)

即使您的编译器总是将其作为原子操作发出,同时从任何其他线程访问num将构成根据C ++ 11和C ++ 14标准的数据竞争,并且该程序将未定义行为。

但它比那更糟糕。首先,如上所述,编译器在递增变量时生成的指令可能取决于优化级别。其次,如果++num不是原子的,编译器可能会重新排序num周围的其他内存访问,例如。

int main()
{
  std::unique_ptr<std::vector<int>> vec;
  int ready = 0;
  std::thread t{[&]
    {
       while (!ready);
       // use "vec" here
    });
  vec.reset(new std::vector<int>());
  ++ready;
  t.join();
}

即使我们乐观地假设++ready是“原子”,并且编译器根据需要生成检查循环(正如我所说,它是UB,因此编译器可以自由地删除它,将其替换为无限循环等),编译器可能仍会移动指针赋值,甚至更糟糕的是vector初始化到增量操作后的某个点,从而导致新线程出现混乱。在实践中,如果优化编译器完全删除了ready变量和检查循环,我就不会感到惊讶,因为这不会影响语言规则下的可观察行为(与您的私人希望相反)。

事实上,在去年的C ++会议上,我从两位编译器开发人员那里听说,只要语言规则允许,他们就很乐意实现优化,使天真编写的多线程程序行为不端。如果在正确编写的程序中可以看到性能稍有改善的话。

最后,即使如果你不关心可移植性,并且你的编译器非常好,你使用的CPU很可能是超标量CISC类型,并且会将指令分解成微观操作,重新排序和/或推测性地执行它们,仅限于通过同步诸如(在英特尔)LOCK前缀或内存栅栏之类的原语来限制,以便最大化每秒操作。

总而言之,线程安全编程的自然责任是:

  1. 您的职责是编写在语言规则下具有明确定义的行为的代码(特别是语言标准内存模型)。
  2. 您的编译器的职责是生成在目标体系结构的内存模型下具有相同明确定义(可观察)行为的机器代码。
  3. 您的CPU的职责是执行此代码,以便观察到的行为与其自己的架构的内存模型兼容。
  4. 如果您想以自己的方式执行此操作,它可能仅在某些情况下有效,但了解保修无效,并且您将对任何不需要的结果承担全部责任。 : - )

    PS:正确编写的例子:

    int main()
    {
      std::unique_ptr<std::vector<int>> vec;
      std::atomic<int> ready{0}; // NOTE the use of the std::atomic template
      std::thread t{[&]
        {
           while (!ready);
           // use "vec" here
        });
      vec.reset(new std::vector<int>());
      ++ready;
      t.join();
    }
    

    这是安全的,因为:

    1. ready的检查无法根据语言规则进行优化。
    2. ++ready 发生在检查之前ready为非零,而其他操作无法围绕这些操作重新排序。这是因为++ready和检查顺序一致,这是C ++内存模型中描述的另一个术语,禁止这种特定的重新排序。因此,编译器不得对指令重新排序,并且还必须告诉CPU它必须不重新排序。在vec增加后将写入推迟到ready顺序一致是语言标准中关于原子的最有力保证。较小(且理论上较便宜)的保证可用,例如通过std::atomic<T>的其他方法,但这些方法绝对仅供专家使用,编译器开发人员可能不会对其进行太多优化,因为很少使用它们。

答案 7 :(得分:7)

在x86计算机有一个CPU的那一天,使用单个指令确保中断不会拆分读/修改/写入,如果内存也不会用作DMA缓冲区,则它是原子的事实上(并且C ++没有提到标准中的线程,因此没有解决)。

当在客户桌面上很少使用双处理器(例如双插槽Pentium Pro)时,我有效地使用它来避免单核机器上的LOCK前缀并提高性能。

今天,它只会帮助对多个线程设置为相同的CPU亲和力,所以你担心的线程只会通过时间片到期并在同一个CPU(核心)上运行另一个线程来发挥作用。这是不现实的。

使用现代x86 / x64处理器,单条指令分为几个微操作,并且内存读写缓冲。因此,在不同CPU上运行的不同线程不仅会将其视为非原子,而且可能会看到有关内存中读取内容的不一致结果以及其他线程在该时间点读取的内容:您需要添加内存围栏恢复理智的行为。

答案 8 :(得分:4)

没有。 https://www.youtube.com/watch?v=31g0YE61PLQ (这只是“办公室”中“否”场景的链接)

您是否同意这可能是该计划的可能输出:

示例输出:

100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100

如果是这样,那么编译器可以自由地为程序提供唯一可能的输出,无论编译器想要什么样的方式。即一个只能推出100s的main()。

这是“假设”规则。

无论输出如何,您都可以以相同的方式考虑线程同步 - 如果线程A执行num++; num--;而线程B重复读取num,则可能的有效交错是线程B从不在num++num--。由于交错有效,编译器可以自由地使可能交错。然后完全删除incr / decr。

这里有一些有趣的含义:

while (working())
    progress++;  // atomic, global

(即想象一些其他线程根据progress更新进度条UI)

编译器可以将其转换为:

int local = 0;
while (working())
    local++;

progress += local;

可能是有效的。但可能不是程序员所希望的: - (

委员会仍在研究这个问题。目前它“有效”,因为编译器不会很多地优化原子。但这种情况正在改变。

即使progress也是易变的,这仍然有效:

int local = 0;
while (working())
    local++;

while (local--)
    progress++;

: - /

答案 9 :(得分:2)

是的,但是......

Atomic不是你想说的。你可能会问错了。

增量肯定是原子。除非存储未对齐(并且由于您没有对齐编译器,否则它不是),它必须在单个缓存行中对齐。缺少特殊的非缓存流指令,每次写入都会通过缓存。完整的缓存行被原子地读取和写入,从来没有任何不同 当然,小于高速缓存行的数据也是以原子方式写入的(因为周围的高速缓存行是)。

它是线程安全的吗?

这是一个不同的问题,至少有两个很好的理由可以用明确的&#34; No!&#34; 来回答。

首先,有可能另一个核心可能在L1中拥有该缓存行的副本(L2和向上通常是共享的,但L1通常是每个核心!),并同时修改该值。当然,这也是原子地发生的,但现在你有两个&#34;正确的&#34; (正确的,原子的,修改的)值 - 现在哪个是真正正确的值?
当然,CPU会以某种方式对其进行排序。但结果可能不是你所期望的。

其次,存在内存排序,或者在保证之前发生不同的措辞。关于原子指令最重要的不是它们是原子。它的订购。

你有可能强制保证所有发生记忆的事情都是在一些有保证的,明确定义的顺序中实现的,你有一个&#34;发生在&#34;之前。保证。这种排序可以是&#34;放松&#34; (读作:完全没有)或严格按照你的需要。

例如,您可以设置指向某个数据块的指针(例如,某些计算的结果),然后原子地发布&#34;数据已准备好&#34;旗。现在,无论谁获取这个标志,都会被认为指针有效。事实上,总是是一个有效的指针,从来没有任何不同。这是因为指针的写入发生在原子操作之前。

答案 10 :(得分:2)

在特定CPU架构上,单个编译器的输出已禁用优化(因为gcc在优化in a quick&dirty example时甚至不会将++编译为add),这似乎意味着增加这种方式是原子并不意味着这是符合标准的(在线程中尝试访问num时会导致未定义的行为),并且无论如何都是错误的,因为add 不是< / em>原子在x86。

请注意,atomics(使用lock指令前缀)在x86(see this relevant answer)上相对较重,但仍然明显小于互斥,这在这个用例中并不十分合适。

在使用-Os编译时,以下结果取自clang ++ 3.8。

通过引用增加int,“常规”方式:

void inc(int& x)
{
    ++x;
}

这编译成:

inc(int&):
    incl    (%rdi)
    retq

增加一个通过引用传递的int,原子方式:

#include <atomic>

void inc(std::atomic<int>& x)
{
    ++x;
}

这个比常规方法复杂得多的例子,只是将lock前缀添加到incl指令中 - 但要注意,如前所述这是便宜。仅仅因为装配看起来很短并不意味着它很快。

inc(std::atomic<int>&):
    lock            incl    (%rdi)
    retq

答案 11 :(得分:-3)

尝试在非x86机器上编译相同的代码,您将很快看到非常不同的装配结果。

num++ 出现原型的原因是因为在x86机器上,递增32位整数实际上是原子的(假设没有发生内存检索)。但这不是c ++标准所保证的,也不是在没有使用x86指令集的机器上的情况。因此,此代码不会因竞争条件而跨平台安全。

即使在x86架构上,您也没有强有力的保证此代码在Race Conditions中是安全的,因为除非特别指示,否则x86不会设置加载和存储到内存。因此,如果多个线程试图同时更新此变量,它们最终可能会增加缓存(过期)值

然后,我们有std::atomic<int>等等的原因是,当您使用不能保证基本计算的原子性的架构时,您有一种机制会迫使编译器生成原子代码。