为了账号安全,请及时绑定邮箱和手机立即绑定

num+在“int num”中可以是原子的吗?

num+在“int num”中可以是原子的吗?

C++ C
达令说 2019-06-16 13:42:05
num+在“int num”中可以是原子的吗?一般情况下,int num, num++(或++num),作为读-修改-写入操作,是非原子..但是我经常看到编译器,例如GCC,为它生成以下代码(试试这里):因为第5行对应于num++是一条指令,我们能得出结论吗num++ 是原子的在这种情况下?如果是这样的话,这是否意味着num++可以在并发(多线程)方案中使用,而不存在任何数据竞争的危险。(例如,我们不需要去做,std::atomic<int>加上相关的成本,因为它是原子的)?更新注意这个问题是不是否增量是原子(它不是,也是这个问题的开场白)。关键是它是否能,会,可以在特定情况下,即是否可以在某些情况下利用单指令性质来避免lock前缀。而且,正如公认的答案在关于单处理器计算机的一节中提到的,以及这个答案在评论和其他人的解释中,它可以(虽然没有使用C或C+)。
查看完整描述

3 回答

?
一只斗牛犬

TA贡献1784条经验 获得超2个赞

.现在让我们启用优化:


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

现在,每项修改都是:


在另一个线程中可以观察到,并且

尊重发生在其他线程中的类似修改。

原子性不只是在指令级,它涉及到从处理器到缓存,到内存和返回的整个管道。


进一步信息

的更新的优化效果std::atomicS.


c+标准具有“似乎”规则,允许编译器重新排序代码,甚至可以重写代码,条件是结果具有完全相同效果(包括副作用),就好像它只是简单地执行了您的代码。


如果规则是保守的,特别涉及原子。


考虑:


void incdec(int& num) {

    ++num;

    --num;

}

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


void incdec(int&) {

    // nada

}

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


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


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


查看完整回答
反对 回复 2019-06-16
?
BIG阳

TA贡献1859条经验 获得超6个赞

没有许多复杂的指示,如add DWORD PTR [rbp-4], 1很像CISC的风格。

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

AGENT 1          AGENT 2load X              
inc C
                 load X
                 inc C
                 store X
store X

x只增加一次。


查看完整回答
反对 回复 2019-06-16
  • 3 回答
  • 0 关注
  • 719 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信