C++中的 i++ 和 ++i 这两种自增运算不是原子操作

前言

C++中的 i++++i 这两种自增运算是不是原子操作,突然被这么一问竟有点不知所措,这么“简单”的操作应该是原子的吧,但是好像有读又有写应该不是原子操作,原子操作就是那种刷一下就能完成的操作,准确来描述就是一个操作不可再分,要完成都完成不能存在中间态,咦?怎么听起来和事务这么像?那么 i++++i 是不是原子操作我们看它是否满足不可再分就行了。

原子操作

怎么看是否可再分呢?想到一个办法,看一个操作是否可再分,直接看汇编是不是就行了,比如一个赋值语句:

int main()
{
    int i = 110;
    return 0;
}

使用 x86-64 gcc 13.1编译后生成的汇编:

main:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], 110
        mov     eax, 0
        pop     rbp
        ret

int i = 110; 被汇编成了 mov DWORD PTR [rbp-4], 110 看起来是一句,没啥问题,再看 i++:

int main()
{
    int i = 110;
    i++;
    return 0;
}
main:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], 110
        add     DWORD PTR [rbp-4], 1
        mov     eax, 0
        pop     rbp
        ret

i++ 也被汇编成了一句 add DWORD PTR [rbp-4], 1,居然也是一句,那么这是原子的吗?我们换个编译器看看,使用 x64 msvc 19.35 生成的汇编如下:

i$ = 0
main    PROC
$LN3:
        sub     rsp, 24
        mov     DWORD PTR i$[rsp], 110                    ; 0000006eH
        mov     eax, DWORD PTR i$[rsp]
        inc     eax
        mov     DWORD PTR i$[rsp], eax
        xor     eax, eax
        add     rsp, 24
        ret     0
main    ENDP

看吧,这里被翻译成了3句,这肯定不是原子操作了,那返回来看在gcc编译时生成 add DWORD PTR [rbp-4], 1的这一句,就是原子操作吗?

准确来表述是这样的 add DWORD PTR [rbp-4], 1这条汇编指令本身是原子的,但是在多线程环境中,对于变量的自增操作需要使用适当的同步机制(如互斥锁、原子类型等)来确保原子性和线程安全性。

如果在单核机器上,上述不加锁不会有问题,但到了多核机器上,这个不加锁同样会带来意外后果,两个CPU可以同时执行这条指令,但是两个执行以后,却可能出现只自加了一次

证明++i不是原子操作的例子

写个简单的例子,两个线程同时执行i++自增操作,看最后的结果是否符合预期:

#include <iostream>
#include <thread>

int val = 0;

void f1(int n)
{
    for (int i = 0; i < n; ++i) ++val;
}

int main(int argc, char* argv[])
{
    int n = 100000000;
    if (argc > 1) n = atoi(argv[1]);

    std::thread t1(f1, n);
    std::thread t2(f1, n);

    t1.join();
    t2.join();

    std::cout << "The final value is [" << val << "] for 2 threads running [" << n << "] times." << std::endl;

    return 0;
}

执行结果如下:

$ ./iplusplus 1000
The final value is [2000] for 2 threads running [1000] times.

$ ./iplusplus 10000
The final value is [20000] for 2 threads running [10000] times.

$ ./iplusplus 100000
The final value is [117784] for 2 threads running [100000] times.

$ ./iplusplus 1000000
The final value is [1271769] for 2 threads running [1000000] times.

从运行结果得知,起初1000次和10000次还没出现竞态条件问题,当次数扩大到100000次时,2个线程最终自增的结果只有117784

保证原子操作

还是上面的例子,怎样改成原子操作呢?这时可以利用 std::atomic 模板类,只需将上述例子中的 val 变量修改成 std::atomic<int> val(0); 即可:

#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> val(0);

void f1(int n)
{
    for (int i = 0; i < n; ++i) ++val;
}

int main(int argc, char* argv[])
{
    int n = 100000000;
    if (argc > 1) n = atoi(argv[1]);

    std::thread t1(f1, n);
    std::thread t2(f1, n);

    t1.join();
    t2.join();

    std::cout << "The final value is [" << val << "] for 2 threads running [" << n << "] times." << std::endl;

    return 0;
}

再编译运行试试:

$ ./iplusplus 100
The final value is [200] for 2 threads running [100] times.

$ ./iplusplus 10000
The final value is [20000] for 2 threads running [10000] times.

$ ./iplusplus 1000000
The final value is [2000000] for 2 threads running [1000000] times.

$ ./iplusplus 100000000
The final value is [200000000] for 2 threads running [100000000] times.

这样就解决了i++不是原子操作的问题,这里还可以将 ++val 写成 val.fetch_add(1) 表示原子加,其实 std::atomic 类实现了 operator++ 调用的就是 fetch_add(1)

_GLIBCXX_ALWAYS_INLINE value_type
operator++(int) const noexcept
{ return fetch_add(1); }

value_type
operator++() const noexcept
{ return __atomic_impl::__add_fetch(_M_ptr, value_type(1)); }

总结

  • i++++i 不是原子操作,执行命令时包含内存读取,变量递增,回写内存三步,所以存在 data race
  • 即使被汇编成一句 add DWORD PTR [rbp-4], 1一句代码在多核CPU上也会导致结果的不确定性或错误
  • 想要 i++ 变成原子操作只需要定义成 std::atomic 模板类的对象即可,逻辑代码几乎无需修改
==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

我们常常把求之不得的东西称之为理想~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AlbertS

常来“玩”啊~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值