原子性load和普通的load有什么区别吗?

新手上路,请多包涵

比如C++ atomic load和普通的直接读有什么区别吗?有什么场景是只能用atomic load,而不能用直接读呢?

阅读 3.1k
1 个回答
示例代码
#include <iostream>
#include <thread>

using namespace std;

static int g_value = 0;

void thread_function()
{
        for (int i=0; i<100000; ++i)
                g_value++;
}

int main()
{
        cout << "begin, g_value : " << g_value << endl;

        std::thread thread_obj_1(thread_function);
        std::thread thread_obj_2(thread_function);

        thread_obj_1.join();
        thread_obj_2.join();

        cout << "end, g_value : " << g_value << endl;

        return 0;
}

输出:

tiansong@tiansong:~/Desktop$ g++ main.cpp -g
tiansong@tiansong:~/Desktop$ ./a.out    # 注意每次输出值都不确定
begin, g_value : 0
end, g_value : 113135
tiansong@tiansong:~/Desktop$ ./a.out 
begin, g_value : 0
end, g_value : 115001
tiansong@tiansong:~/Desktop$ ./a.out 
begin, g_value : 0
end, g_value : 13696

预期的结果应该是 200000, 可实际的结果总是有出入,显然是有问题的。
分析 thread_function 对应汇编文件:

void thread_function()
{
    12c9:       f3 0f 1e fa             endbr64
    12cd:       55                      push   %rbp
    12ce:       48 89 e5                mov    %rsp,%rbp
        for (int i=0; i<100000; ++i)
    12d1:       c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
    12d8:       eb 13                   jmp    12ed <_Z15thread_functionv+0x24>
                g_value++;              ## 注意注意,g_value++ 被分解成了三条汇编码  !!                                                                 
    12da:       8b 05 78 2e 00 00       mov    0x2e78(%rip),%eax        # 4158 <_ZL7g_value>
    12e0:       83 c0 01                add    $0x1,%eax
    12e3:       89 05 6f 2e 00 00       mov    %eax,0x2e6f(%rip)        # 4158 <_ZL7g_value>
        for (int i=0; i<100000; ++i)
    12e9:       83 45 fc 01             addl   $0x1,-0x4(%rbp)
    12ed:       81 7d fc 9f 86 01 00    cmpl   $0x1869f,-0x4(%rbp)
    12f4:       7e e4                   jle    12da <_Z15thread_functionv+0x11>
}

分析:

`g_value++` 被分解成了 3 条汇编码,抽象如下:
A,从内存取值到寄存器
B,加一
C,讲加一后的寄存器值放入内存
👇
当线程间没有同步关系(【每个线程有独立栈空间-保留对应各自的寄存器】):
线程 1 执行到 B 步骤(C之前),此时被线程 2 打断,线程 2 调度执行
线程 2 加一的结果放入内存
线程 1 调度执行开始执行,C!! 此处线程 2 的加一被【覆盖】 !!!!!!!!

从上面可以看出数据竞争导致的问题
在 C/C++ 层面,一条语言往往对应多条汇编语言和机器指令,为了保证完整性,出现了锁用于同步
到了 C++11 出现了 atomic 对单个基础数据类型进行封装,原子类型相比 mutex 更轻量拥有更好的性能


楼主问题的回答:

  1. atomic_load 可以防止出现半成品的中间数值(脏数据)
  2. 多线程访问同一资源 + 有写有读时,两个条件满足时往往需要考虑锁(atomic)
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题