关于c ++:num ++是’int num’的原子吗?

Can num++ be atomic for 'int num'?

在通用int num,for,num++(or,as a ++num)读-写- modify operation is not,原子。但compilers often see for example,the following GCC产生的代码(在这里尝试*):P></

Enter image description hereP></

自从5 corresponds在线,which is one to num++指令,我们可以得出结论num++is that this原子在家吗?P></

如果我知道,我知道-它does that generated均值num++can be used in(并发多线程),没有任何危险的竞赛场景数据(即不让我们need to,for example,the Associated std::atomic和实行成本,因为它的原子啊)?P></

更新P></

is not that this question whether时间增量是原子(它是not,is the opening of the和在线的问题)。这是否是它可以是一个场景,即指令是否可以在一定自然好avoid the cases of the剥削的量lock字头。和答案,as the accepted the section在单处理器机器mentions about this as,as the conversation回答的好的意见,在其与其他解释(尽管它可以不与,C或C + +)。P></


这绝对是C++定义的一种数据竞争,它导致了未定义的行为,即使一个编译器恰好产生了一些目标机器上所希望的代码。为了获得可靠的结果,您需要使用std::atomic,但是如果您不关心重新排序,您可以将它与memory_order_relaxed一起使用。有关使用fetch_add的一些示例代码和asm输出,请参见下文。好的。

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

Since num++ is one instruction (add dword [num], 1), can we conclude that num++ is atomic in this case?

Ok.

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

来自其他CPU的内存操作可以在加载和存储之间全局可见。也就是说,在一个循环中运行add dword [num], 1的两个线程将在彼此的存储中逐步进行。(请参阅@margaret's answer获取一个很好的图表)。从两个线程中的每一个线程增加40K后,在真正的多核x86硬件上,计数器可能只增加了~60K(而不是80K)。好的。

"原子",从希腊词的含义不可分割,意味着没有观察者可以将操作视为单独的步骤。对于所有位同时发生物理/电即时只是一种实现负载或存储的方法,但ALU操作甚至不可能。在我对x86原子性的回答中,我更详细地介绍了纯加载和纯存储,而这个回答主要关注于读-修改-写。好的。

lock前缀可以应用于许多读-修改-写(内存目标)指令,使整个操作对于系统中所有可能的观察者(其他核心和DMA设备,而不是连接到CPU引脚的示波器)具有原子性。这就是它存在的原因。(另请参见本问答)。好的。

所以lock add dword [num], 1是原子的。运行该指令的CPU核心将使缓存线保持在其专用的一级缓存中的修改状态,从加载从缓存读取数据到存储将其结果提交回缓存。根据MESI缓存一致性协议(或多核AMD/Intel CPU分别使用的MOESI/MESIF版本)的规则,这可以防止系统中的任何其他缓存在加载到存储的任何点上拥有缓存线的副本。因此,其他核心的操作似乎发生在之前或之后,而不是在期间。好的。

without the lock字头,一芯线是把所有权of the缓存和后负荷,但在modify恩我们知道,我们的商店,其他商店会成为我们之间globally visible Load和Store。several other answers get this和that没有错,你会得到claim lockof the same conflicting拷贝缓存线。这永远不会发生在我caches与相干系统。>

locked if(指令缓存存储,在线operates spans两线,它需要很多的工作让更多的部分(the changes to both of the对象传递给他们留下来的原子作为观测器,观测器能看到我不拆除。might have to the lock the Whole CPU总线数据存储记忆until the精选。你不要错位原子元!)>

【注释lockthat the字头也进入安全的内存屏障指令(类mfence)停止运行时,在thus给reordering和顺序一致性。杰夫preshing' EEA(S优秀博客。在他的其他posts是太优秀,解释,和很多好的东西clearly of about无锁编程,和其他从x86硬件details to C++规则)。>

在单处理器机器上,或在单进程单线程的指令,在RMW没有lock字头其实是原子。the only for the Way to other队列访问共享变量is for the context to给交换机的CPU,它不能发生在中间of an指令。我知道我在平原dec dword [num]单线程程序synchronize between and its handlers信号,或在多线程程序在单核机器上运行。see the second another半在线问题回答(我*岁,对我解释它,这在更多的细节。>

C + +:back to

这是完全正常的num++without telling the bogus使用编译器编译它,你需要去读-写-单身执行:modify>

1
2
3
4
;; Valid compiler output for num++
mov   eax, [num]
inc   eax
mov   [num], eax

如果你使用this is the value甚likely of the编译器将一num:keep the增量在登记后的现场。如果你知道如何重新检查自己的在线num++compiles changing the围岩结构,可以影响它的尾巴。>

(if the value不需要后,inc dword [num]is preferred;现代cpus将运行在x86指令目的地记忆的RMW least as as的使用教学efficiently三分开。有趣的事实:人真的会因为这gcc -O3 -m32 -mtune=i586P5(奔腾),是不decode复合管道的超标量微操作简单到多元教学方式及以后的P6微结构。see the agner fog' S /微指令的更多信息的指南,and the many useful for x86标签维基链接(包括英特尔的x86 ISA manuals,which are available frehley as PDF))。>

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

允许编译时重新排序。使用std::atomic得到的另一部分是对编译时重新排序的控制,以确保您的num++只有在执行其他操作之后才全局可见。好的。

经典示例:将一些数据存储到缓冲区中,以查看另一个线程,然后设置标志。即使X86免费获取加载/释放存储,您仍然必须告诉编译器不要通过使用EDCOX1(1)来重新排序。好的。

您可能期望此代码将与其他线程同步:好的。

1
2
3
4
// 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+EDCOX1,5)不是STD:原子的有用替代物。std::atomic确实使编译器假定可以异步修改内存中的值,类似于volatile,但它的功能远不止这些。另外,正如与@richard hodges讨论的那样,volatile std::atomic foostd::atomic foo不同。)好的。

将非原子变量上的数据竞争定义为未定义的行为,这使得编译器仍然可以将负载和接收器存储从循环中提升出来,并且可以对多个线程可能引用的内存进行许多其他优化。(有关ub如何启用编译器优化的详细信息,请参阅此llvm日志。)好的。

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

这就是GCC在x86上实际所做的,对于一些在std::atomic全局变量上运行的函数。好的。

请参阅godbolt编译器资源管理器上格式化良好的源代码+汇编语言代码。您可以选择其他目标体系结构,包括ARM、MIPS和PowerPC,以查看从原子中为这些目标获得的汇编语言代码类型。好的。

1
2
3
4
5
6
7
8
9
10
11
12
#include
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.

好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 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上获得良好的性能是至关重要的。JeffPreshing的内存重新排序行为显示了不使用mFence的后果,真实代码显示了在真实硬件上发生的重新排序。好的。

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

关于同一主题的单独问题:为什么编译器不合并冗余的std::atomic写入?,我的回答重申了我在下面写的很多东西。好的。

当前编译器实际上并没有这样做,但不是因为它们不被允许。C++ WG21/P062R1:编译器应该何时优化原子?讨论了许多程序员认为编译器不会做出"令人惊讶的"优化的期望,以及标准可以给程序员控制。N4455讨论了许多可以优化的例子,包括这一个。它指出,内联和持续传播可以引入像fetch_or(0)这样的东西,即使在原始源没有任何明显的冗余原子操作的情况下,它也可以变成一个load()(但仍然具有获取和释放语义)。好的。

编译器不这样做的真正原因是:(1)没有人编写过复杂的代码,这样编译器就可以安全地(而不会出错)这样做;(2)它可能违反了最不令人惊讶的原则。无锁代码很难一开始就正确地编写。所以不要随意使用原子武器:它们既不便宜,也不会优化很多。但是,使用std::shared_ptr避免冗余的原子操作并不总是容易的,因为它没有非原子版本(尽管这里的一个答案给出了一个简单的方法来定义gcc的shared_ptr_unsynchronized)。好的。

回到num++; num-=2;编译,就好像它是num--一样:除非numvolatile std::atomic,否则编译器可以这样做。如果可以重新排序,则"假设"规则允许编译器在编译时决定它总是以这种方式发生。没有什么能保证观察者能看到中间值(num++结果)。好的。

即,如果这些操作之间没有任何全局可见的排序与源的排序要求兼容(根据抽象机器的C++规则,而不是目标体系结构),编译器可以发射一个EDOCX1×9,而不是EDCOX1,10,EDCX1,11。好的。

num++; num--不能消失,因为它仍然与查看num的其他线程有同步关系,它既是一个获取负载,又是一个发布存储,不允许在此线程中重新排序其他操作。对于x86,这可能能够编译为mfence,而不是lock add dword [num], 0(即num += 0)。好的。

如PR0062中所讨论的,在编译时更积极地合并非相邻的原子操作可能是不好的(例如,进度计数器只在结束时更新一次,而不是每次迭代),但它也可以帮助性能不受负面影响(例如,在创建和销毁shared_ptr的副本时,跳过原子inc/dec-of-ref计数,如果编译器能够证明另一个shared_ptr对象在临时对象的整个生命周期内都存在的话。)好的。

当一个线程立即解锁和重新锁定时,即使是num++; num--合并也可能损害锁实现的公平性。如果它从未在ASM中真正释放过,那么即使是硬件仲裁机制也不会给另一个线程在这一点上获取锁的机会。好的。

在当前的GCC6.2和CLANG3.9中,即使在最明显的优化情况下使用memory_order_relaxed,您仍然可以获得单独的lockED操作。(Godbolt编译器资源管理器,以便查看最新版本是否不同。)好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
2
f():
        rep ret

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

1
2
3
4
5
6
7
8
9
void f(int& num)
{
  num = 0;
  num++;
  --num;
  num += 6;
  num -=5;
  --num;
}

结果:

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

另一个观察线程(甚至忽略缓存同步延迟)没有机会观察单个更改。

比较:

1
2
3
4
5
6
7
8
9
10
11
#include

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

如果结果是:

1
2
3
4
5
6
7
8
9
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

现在,每次修改都是:

  • 在另一条线中可观察到的,以及
  • 尊重在其他线程中发生的类似修改。
  • 原子性不只是在指令级别,它涉及从处理器、通过缓存到内存和内存的整个管道。

    进一步信息

    关于优化std::atomic更新的效果。

    C++标准具有"以貌取人"的规则,它允许编译器重新排序代码,甚至可以重写代码,只要结果具有完全相同的可观察的效果(包括副作用),就好像它简单地执行了代码一样。

    假设规则是保守的,尤其涉及原子论。

    考虑:

    1
    2
    3
    4
    void incdec(int& num) {
        ++num;
        --num;
    }

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

    1
    2
    3
    void incdec(int&) {
        // nada
    }

    这是因为在C++内存模型中,不可能有另一个线程观察增量的结果。当然,如果numvolatile(可能影响硬件行为),情况会有所不同。但是在这种情况下,这个函数将是修改这个内存的唯一函数(否则程序是格式错误的)。

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

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

    num是一种原子。它的变化必须能被其他正在观察的线程观察到。这些线程本身所做的更改(例如在递增和递减之间将值设置为100)将对num的最终值产生非常深远的影响。

    这是一个演示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #include <thread>
    #include

    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;
        }
    }

    样品输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    99
    99
    99
    99
    99
    100
    99
    99
    100
    100
    100
    100
    99
    99
    100
    99
    99
    100
    100
    99


    没有很多并发症,像add DWORD PTR [rbp-4], 1这样的指令是非常cisc风格的。

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

    1
    2
    3
    4
    5
    6
    7
    8
    AGENT 1          AGENT 2

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

    X只递增一次。


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

    添加指令的原子变量称为lock xadd


    Since line 5, which corresponds to num++ is one instruction, can we conclude that num++ is atomic in this case?

    基于"逆向工程"生成的装配得出结论是危险的。例如,您似乎已在禁用优化的情况下编译了代码,否则编译器会丢弃该变量或直接将1加载到它,而不调用operator++。因为生成的程序集可能会发生显著的变化,基于优化标志、目标CPU等,所以您的结论是基于sand的。

    另外,您认为一条汇编指令意味着一个操作是原子的想法也是错误的。这个add在多CPU系统上,甚至在x86体系结构上,都不是原子的。


    即使你的编译器总是把它作为原子操作来发射,同时从任何其他线程中访问EDCOX1×0,将根据C++ 11和C++ 14标准构成一个数据竞争,程序将有未定义的行为。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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的初始化到p增量操作后的点,导致新线程中的混乱。实际上,如果一个优化编译器完全删除了ready变量和检查循环,我一点也不会感到惊讶,因为这不会影响语言规则下的可观察行为(与您的私人希望相反)。

    事实上,在去年的会议C++会议上,我从两个编译器开发人员那里听说,他们非常乐意执行优化,使语言编写的多线程程序行为不当,只要语言规则允许,即使在正确编写的程序中看到微小的性能改进。

    最后,即使您不关心可移植性,而且您的编译器非常好,您使用的CPU很可能是一种超标量的cisc类型,它会将指令分解为微操作、重新排序和/或推测地执行它们,在某种程度上,仅限于同步诸如(在Intel上)LOCK前缀或mem之类的原语。ORY围栏,以最大化每秒操作。

    长话短说,线程安全编程的自然责任是:

  • 您的职责是编写在语言规则(特别是语言标准内存模型)下具有良好定义行为的代码。
  • 编译器的职责是生成在目标体系结构的内存模型下具有相同定义良好(可观察)行为的机器代码。
  • CPU的职责是执行这段代码,以便观察到的行为与它自己的体系结构的内存模型兼容。
  • 如果你想用你自己的方式来做,它可能在某些情况下是有效的,但是要理解保证是无效的,并且你将对任何不想要的结果负全部责任。-)

    附:正确书写的示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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();
    }

    这是安全的,因为:

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

  • 在单核x86机器上,相对于CPU1上的其他代码,add指令通常是原子的。中断不能把一条指令从中间分割开。

    无序执行是为了在一个内核中保持一个指令一次执行一个指令的假象,所以在同一个CPU上运行的任何指令要么完全发生在添加之前,要么完全发生在添加之后。

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

    如果目标是一台小型嵌入式PC,并且没有计划将代码移动到其他任何地方,那么"添加"指令的原子性质就可以被利用。另一方面,操作本身具有原子性的平台变得越来越稀缺。

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

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


    在X86计算机有一个CPU的日子里,使用一个指令确保中断不会分割读/修改/写,如果内存也不被用作DMA缓冲器,它实际上是原子的(C++没有提到标准中的线程,所以没有解决这个问题)。

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

    今天,它只对设置为相同CPU关联性的多个线程有帮助,因此您担心的线程将只通过在同一个CPU(核心)上过期并运行另一个线程来发挥作用。这不现实。

    在现代x86/X64处理器中,单个指令被分解成若干个微操作,并且内存读写被缓冲。因此,在不同CPU上运行的不同线程不仅会将此视为非原子的,而且可能会看到与它从内存中读取的内容以及它假定其他线程已读取到该时间点的内容有关的不一致的结果:您需要添加内存边界来恢复正常的行为。


    不。https://www.youtube.com/watch?V= 31 G0YY61PLQ(这只是从"办公室"到"不"场景的链接)

    您是否同意这是程序的一个可能输出:

    样品输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    100
    100
    100
    100
    100
    100
    100
    100
    100
    100
    100
    100
    100
    100
    100
    100
    100
    100
    100
    100

    如果是这样,那么编译器就可以自由地以编译器想要的任何方式将其作为程序的唯一可能输出。也就是说,一个只输出100秒的主函数。

    这是"好像"规则。

    不管输出是什么,你都可以用同样的方法来考虑线程同步——如果线程A执行num++; num--;,而线程B重复读取num,那么一个可能的有效交织就是线程B从未在num++num--之间读取。由于这种交织是有效的,编译器可以自由地使之成为唯一可能的交织。完全移除增量/增量。

    这里有一些有趣的含义:

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

    (例如,假设其他线程更新了基于progress的进度条用户界面)

    编译器能否将其转换为:

    1
    2
    3
    4
    5
    int local = 0;
    while (working())
        local++;

    progress += local;

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

    委员会仍在研究这方面的问题。目前它"有效"是因为编译器不太优化原子。但这正在改变。

    即使progress也不稳定,这仍然有效:

    1
    2
    3
    4
    5
    6
    int local = 0;
    while (working())
        local++;

    while (local--)
        progress++;

    - -


    在特定的CPU体系结构上,禁用优化的单个编译器的输出(因为在快速和肮脏的示例中进行优化时,GCC甚至不将++编译为add)似乎意味着以原子方式递增并不意味着这符合标准(在尝试访问E时,会导致未定义的行为)。线程中的docx1(2),无论如何都是错误的,因为add在x86中不是原子的。

    注意,atomics(使用lock指令前缀)在x86上相对比较重(请参阅相关答案),但仍然明显小于mutex,这在这个用例中并不太合适。

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

    通过引用递增一个int,"常规"方法:

    1
    2
    3
    4
    void inc(int& x)
    {
        ++x;
    }

    这包括:

    1
    2
    3
    inc(int&):
        incl    (%rdi)
        retq

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

    1
    2
    3
    4
    5
    6
    #include

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

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

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

    是的,但是……

    原子不是你想说的。你可能问错了。

    增量当然是原子的。除非存储未对齐(而且由于您保持了对编译器的对齐,所以它没有对齐),否则它必须在单个缓存线内对齐。缺少特殊的非缓存流指令,每个写操作都会通过缓存。完整的缓存线被原子地读写,没有任何不同。当然,小于cache line的数据也是原子写入的(因为周围的缓存线是)。

    它是线程安全的吗?

    这是一个不同的问题,至少有两个很好的理由可以肯定地回答"不!".

    首先,有可能另一个内核在l1中拥有该缓存线的副本(l2和向上通常是共享的,但l1通常是每个内核!),并同时修改该值。当然,这也是原子性的,但是现在你有了两个"正确的"(正确的,原子性的,修改过的)值——现在哪个值是真正正确的?当然,CPU会以某种方式进行处理。但结果可能不是你所期望的。

    第二,有内存排序,或者在保证之前有不同的措辞。原子指令最重要的不是原子指令。这是订货。

    你有可能强制保证所有发生记忆的事情都是在一些有保证的、明确定义的订单中实现的,在那里你有一个"以前发生过的"保证。这个排序可以是"放松"(读为:根本没有)或严格按照你的需要。

    例如,可以设置指向某个数据块的指针(例如,一些计算的结果),然后原子性地释放"数据就绪"标志。现在,任何获得这个标志的人都会认为指针是有效的。事实上,它始终是一个有效的指针,不会有任何不同。这是因为指针的写入发生在原子操作之前。


    当编译器只使用一条指令来执行增量,并且您的机器是单线程的,那么您的代码是安全的。^ ^


    尝试在非x86计算机上编译相同的代码,您很快就会看到非常不同的汇编结果。

    num++看起来是原子的原因是因为在x86机器上,增加32位整数实际上是原子的(假设没有进行内存检索)。但这既不是由C++标准保证的,也不是在不使用x86指令集的机器上的情况。所以这个代码不是跨平台安全的,不受比赛条件的影响。

    即使在x86体系结构上,您也不能很好地保证此代码不受竞争条件的影响,因为除非特别指示,否则x86不会将加载和存储设置到内存中。因此,如果多个线程试图同时更新这个变量,它们可能最终会增加缓存(过时)值。

    那么,我们有EDOCX1[1]等等的原因是,当您使用的体系结构不能保证基本计算的原子性时,您有一种机制可以强制编译器生成原子代码。