T、volatile T 和 std::atomic<T> 之间有什么区别?

如何解决T、volatile T 和 std::atomic<T> 之间有什么区别?

鉴于下面的示例打算等到另一个线程将 42 存储在共享变量 shared 中而没有锁定且不等待线程终止,为什么 volatile T 或 {{1}需要或推荐以保证并发正确性?

std::atomic<T>

使用 GCC 4.8.5 和默认选项,示例按预期工作。

解决方法

测试似乎表明样本是正确的但事实并非如此。类似的代码很容易在生产中结束,甚至可以完美运行多年。

我们可以从使用 -O3 编译示例开始。现在,样本无限期地挂起。 (默认是-O0,没有优化/调试一致性,有点类似于让每个变量都volatilewhich is the reason the test didn't reveal the code as unsafe。)

要找到根本原因,我们必须检查生成的程序集。首先,对应于未优化工作二进制文件的基于 GCC 4.8.5 -O0 的 x86_64 程序集:

        // Thread B:
        // shared = 42;
        movq    -8(%rbp),%rax
        movq    (%rax),%rax
        movq    $42,(%rax)

        // Thread A:
        // while (shared != 42) {
        // }
.L11:
        movq    -32(%rbp),%rax     # Check shared every iteration
        cmpq    $42,%rax
        jne     .L11

线程 B 在 42 中执行值 shared 的简单存储。 线程 A 为每次循环迭代读取 shared,直到比较表明相等。

现在,我们将其与 -O3 结果进行比较:

        // Thread B:
        // shared = 42;
        movq    8(%rdi),(%rax)

        // Thread A:
        // while (shared != 42) {
        // }
        cmpq    $42,(%rsp)         # check shared once
        je      .L87                # and skip the infinite loop or not
.L88:
        jmp     .L88                # infinite loop
.L87:

-O3 相关的优化用单个比较替换循环,如果不相等,则用无限循环来匹配预期行为。使用 GCC 10.2,优化了循环。 (与 C 不同,没有副作用或易失性访问的无限循环在 C++ 中是未定义的行为。)

问题在于编译器及其优化器不知道实现的并发影响。因此,结论必须是 shared 不能在线程 A 中改变——循环相当于死代码。 (或者换句话说,数据竞争是 UB,并且优化器可以假设程序不会遇到 UB。如果您正在读取一个非原子变量,那一定意味着没有其他人在写它。这个是什么允许编译器从循环中提升负载,以及类似的接收器存储,对于非共享变量的正常情况,这是非常有价值的优化。)

解决方案要求我们向编译器传达 shared 参与线程间通信。实现这一点的一种方法可能是volatile。尽管 volatile 的实际含义因编译器而异,并且保证(如果有)是特定于编译器的,但普遍的共识是 volatile 阻止编译器在基于寄存器的缓存方面优化易失性访问。这对于与硬件交互并在并发编程中占有一席之地的低级代码至关重要,尽管由于 std::atomic 的引入而呈下降趋势。

使用volatile int64_t shared,生成的指令变化如下:

        // Thread B:
        // shared = 42;
        movq    24(%rdi),(%rax)

        // Thread A:
        // while (shared != 42) {
        // }
.L87:
        movq    8(%rsp),%rax
        cmpq    $42,%rax
        jne     .L87

循环不能再被消除,因为必须假设 shared 发生了变化,即使没有代码形式的证据。因此,该示例现在适用于 -O3

如果 volatile 解决了问题,您为什么还需要 std::atomic?与无锁代码相关的两个方面使 std::atomic 变得必不可少:内存操作原子性和内存顺序。

为了构建加载/存储原子性的案例,我们回顾了使用 GCC4.8.5 -O3 -m32(32 位版本)为 volatile int64_t shared 编译的生成程序集:

        // Thread B:
        // shared = 42;
        movl    4(%esp),%eax
        movl    12(%eax),%eax
        movl    $42,(%eax)
        movl    $0,4(%eax)

        // Thread A:
        // while (shared != 42) {
        // }
.L88:                               # do {
        movl    40(%esp),%eax
        movl    44(%esp),%edx
        xorl    $42,%eax
        movl    %eax,%ecx
        orl     %edx,%ecx
        jne     .L88                # } while(shared ^ 42 != 0);

对于 32 位 x86 代码生成,64 位加载和存储通常分为两条指令。对于单线程代码,这不是问题。对于多线程代码,这意味着另一个线程可以看到 64 位内存操作的部分结果,为意外的不一致留出空间,这些不一致可能不会 100% 的时间导致问题,但可能会随机发生并且出现概率受周围代码和软件使用模式的严重影响。即使 GCC 选择生成默认保证原子性的指令,这仍然不会影响其他编译器,并且可能不适用于所有支持的平台。

为了防止在所有情况下以及跨所有编译器和支持的平台进行部分加载/存储,可以使用 std::atomic。让我们回顾一下 std::atomic 如何影响生成的程序集。更新后的示例:

#include <atomic>
#include <cassert>
#include <cstdint>
#include <thread>

int main()
{
  std::atomic<int64_t> shared;
  std::thread thread([&shared]() {
    shared.store(42,std::memory_order_relaxed);
  });
  while (shared.load(std::memory_order_relaxed) != 42) {
  }
  assert(shared.load(std::memory_order_relaxed) == 42);
  thread.join();
  return 0;
}

生成的基于 GCC 10.2 的 32 位程序集 (-O3: https://godbolt.org/z/8sPs55nzT):

        // Thread B:
        // shared.store(42,std::memory_order_relaxed);
        movl    $42,%ecx
        xorl    %ebx,%ebx
        subl    $8,%esp
        movl    16(%esp),%eax
        movl    4(%eax),%eax       # function arg: pointer to  shared
        movl    %ecx,(%esp)
        movl    %ebx,4(%esp)
        movq    (%esp),%xmm0       # 8-byte reload
        movq    %xmm0,(%eax)       # 8-byte store to  shared
        addl    $8,%esp

        // Thread A:
        // while (shared.load(std::memory_order_relaxed) != 42) {
        // }
.L9:                                # do {
        movq    -16(%ebp),%xmm1       # 8-byte load from shared
        movq    %xmm1,-32(%ebp)       # copy to a dummy temporary
        movl    -32(%ebp),%edx
        movl    -28(%ebp),%ecx        # and scalar reload
        movl    %edx,%eax
        movl    %ecx,%eax
        orl     %eax,%edx
        jne     .L9                 # } while(shared.load() ^ 42 != 0);

为了保证加载和存储的原子性,编译器发出一个 8 字节的 SSE2 movq instruction(到/从 128 位 SSE 寄存器的下半部分)。此外,程序集显示即使删除了 volatile,循环仍然完好无损。

通过在示例中使用 std::atomic,可以保证

  • std::atomic 加载和存储不受基于寄存器的缓存
  • std::atomic 加载和存储不允许观察部分值

C++ 标准根本不讨论寄存器,但确实说明:

实现应该使原子存储在合理的时间内对原子负载可见。

虽然这为解释留下了空间,但在迭代中缓存 std::atomic 负载,例如在我们的示例中触发(没有 volatile 或 atomic)显然是一种违规 - 存储可能永远不会变得可见。当前编译器 don't even optimize atomics within one block,例如在同一迭代中进行 2 次访问。

在 x86 上,自然对齐的加载/存储(其中地址是加载/存储大小的倍数)为 atomic up to 8 bytes without special instructions。这就是 GCC 能够使用 movq 的原因。

硬件可能不直接支持带有大 atomic<T>

T,在这种情况下,编译器可以回退 to using a mutex

某些平台上的大 T(例如 2 个寄存器的大小)可能需要原子 RMW 操作(如果编译器不简单地回退到锁定),有时提供的大小大于保证原子性的最大高效纯负载/纯存储。 (例如,在 x86-64、lock cmpxchg16 或 ARM ldrexd/strexd 重试循环上)。单指令原子 RMW(如 x86 使用)internally involve a cache line lock or a bus lock。例如,对于 x86 的旧版 clang -m32 将使用 lock cmpxchg8b 而不是 movq 用于 8 字节纯加载或纯存储。

上面提到的第二个方面是什么?std::memory_order_relaxed 是什么意思? 编译器和 CPU 都可以重新排序内存操作以优化效率。重新排序的主要约束是所有加载和存储都必须按照代码给出的顺序(程序顺序)执行。因此,在线程间通信的情况下,尽管重新排序尝试,但必须考虑内存顺序以建立所需的顺序。可以为 std::atomic 加载和存储指定所需的内存顺序。 std::memory_order_relaxed 不强加任何特定顺序。

互斥原语强制执行特定的内存顺序(获取-释放顺序),以便内存操作保持在锁范围内,并且保证先前锁所有者执行的存储对后续锁所有者可见。因此,使用锁,这里提出的所有方面都可以通过使用锁定工具来解决。一旦您打破了提供的舒适锁,您就必须注意后果和影响并发正确性的因素。

尽可能明确地说明线程间通信是一个很好的起点,以便编译器了解加载/存储上下文并可以相应地生成代码。只要有可能,prefer std::atomic<T>std::memory_order_relaxed(除非场景要求特定的内存顺序)到 volatile T(当然还有 T)。此外,尽可能不要推出自己的无锁代码,以降低代码复杂性并最大限度地提高正确性。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-
参考1 参考2 解决方案 # 点击安装源 协议选择 http:// 路径填写 mirrors.aliyun.com/centos/8.3.2011/BaseOS/x86_64/os URL类型 软件库URL 其他路径 # 版本 7 mirrors.aliyun.com/centos/7/os/x86
报错1 [root@slave1 data_mocker]# kafka-console-consumer.sh --bootstrap-server slave1:9092 --topic topic_db [2023-12-19 18:31:12,770] WARN [Consumer clie
错误1 # 重写数据 hive (edu)&gt; insert overwrite table dwd_trade_cart_add_inc &gt; select data.id, &gt; data.user_id, &gt; data.course_id, &gt; date_format(
错误1 hive (edu)&gt; insert into huanhuan values(1,&#39;haoge&#39;); Query ID = root_20240110071417_fe1517ad-3607-41f4-bdcf-d00b98ac443e Total jobs = 1
报错1:执行到如下就不执行了,没有显示Successfully registered new MBean. [root@slave1 bin]# /usr/local/software/flume-1.9.0/bin/flume-ng agent -n a1 -c /usr/local/softwa
虚拟及没有启动任何服务器查看jps会显示jps,如果没有显示任何东西 [root@slave2 ~]# jps 9647 Jps 解决方案 # 进入/tmp查看 [root@slave1 dfs]# cd /tmp [root@slave1 tmp]# ll 总用量 48 drwxr-xr-x. 2
报错1 hive&gt; show databases; OK Failed with exception java.io.IOException:java.lang.RuntimeException: Error in configuring object Time taken: 0.474 se
报错1 [root@localhost ~]# vim -bash: vim: 未找到命令 安装vim yum -y install vim* # 查看是否安装成功 [root@hadoop01 hadoop]# rpm -qa |grep vim vim-X11-7.4.629-8.el7_9.x
修改hadoop配置 vi /usr/local/software/hadoop-2.9.2/etc/hadoop/yarn-site.xml # 添加如下 &lt;configuration&gt; &lt;property&gt; &lt;name&gt;yarn.nodemanager.res