AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • 主页
  • 系统&网络
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • 主页
  • 系统&网络
    • 最新
    • 热门
    • 标签
  • Ubuntu
    • 最新
    • 热门
    • 标签
  • Unix
    • 最新
    • 标签
  • DBA
    • 最新
    • 标签
  • Computer
    • 最新
    • 标签
  • Coding
    • 最新
    • 标签
主页 / coding / 问题 / 79594410
Accepted
Ahmed AEK
Ahmed AEK
Asked: 2025-04-27 05:06:58 +0800 CST2025-04-27 05:06:58 +0800 CST 2025-04-27 05:06:58 +0800 CST

在没有同步的情况下读取原子指针是否安全?

  • 772

假设一个原子指针永远不会为 nullptr ,那么在没有同步的情况下读取它是否安全?就像下面的代码一样,假设有两个线程writer同时运行reader。

std::atomic<int>* g_atomic = new std::atomic<int>{};

void writer()
{
    for (int i = 0; i < 101; i++)
    {
        auto* new_atomic = new std::atomic<int>{i};
        std::atomic_thread_fence(std::memory_order_seq_cst); // memory barrier.
        g_atomic = new_atomic; // ignore the memory leak
    }
}

void reader()
{
    auto value = g_atomic->load();
    while (value < 100)
    {
        assert(value >= 0 && value <= 100);
        value = g_atomic->load();
    }
}

我所说的安全是指,我将始终读取从 0 到 100 的值,我不会读取无效指针或在初始化之前读取指向的对象。

我的直觉告诉我这是安全的,因为

  1. 在所有架构上,指针都是以原子方式读取或写入的。
  2. 指向的值是原子读取的,必须从 RAM 中获取,并且写入之前的内存屏障保证 RAM 始终正确。

那么,这安全吗?也许只适用于所有常见的架构?

c++
  • 2 2 个回答
  • 86 Views

2 个回答

  • Voted
  1. Best Answer
    Peter Cordes
    2025-04-27T05:36:32+08:002025-04-27T05:36:32+08:00

    在所有架构上,指针都是以原子方式读取或写入的。

    是的,用汇编语言。但你用的是 C++ 语言,重要的是语言的内存模型及其抽象机。 你的代码存在由并发非同步写入+读取引起的数据竞争 UB g_atomic。

    实际上,读取循环会将 的负载提升g_atomic出循环之外。由于它不是原子的,因此另一个线程修改它就是不合理的,编译器可以假设这种情况不会发生。换句话说,对非原子变量进行数据竞争是 UB,这使得编译器能够继续对单线程代码进行重要的优化。(它们不知道哪些全局变量是共享的,哪些不是。)

    在 C++ 中,如果您想要获得所希望的行为,则必须使用 来请求它std::atomic< T* >。如果不需要任何排序,
    则使用store 和 load 。relaxed

    但实际上你确实需要排序,这样你就不会在指向的对象完全构造之前发布指针。你使用了一个慢速指令thread_fence(seq_cst)来实现这一点;实际上你只需要release,最好是.store(val, release)使用单独的栅栏指令,这样它就可以编译为AArch64,stlr而不是单独的屏障指令。

    在你的情况下,也T就是。所以你的代码应该像这样写,才能编译成你认为从原始代码中得到的汇编代码。我将其重命名为 ,因为它和它指向的对象都必须是原子的。std::atomic<int>std::atomic_intg_atomicg_ptr

    (实际上,您不需要指向的对象是原子的,这样就可以保证安全。 因此std::atomic< int* > g_ptr;,假设您不会有线程写入这些指向的对象。由于写入器和读取器之间的同步,初始化发生在读取之前。这里并不正式,因为我relaxed在读取器中使用了 because ,因为consume它已被弃用,并且在实践中可以编译为正确的 asm。)

    #include <atomic>
    using std::memory_order::relaxed, std::memory_order::release;
    std::atomic<int> initval = 0;
    std::atomic< std::atomic<int>* > g_ptr = &initval;
    
    void writer()
    {
        for (int i = 0; i < 101; i++)
        {
            auto* new_atomic = new std::atomic<int>{i};
            //std::atomic_thread_fence(std::memory_order_release);
            g_ptr.store(new_atomic, release);
        }
    }
    
    void reader()
    {
        auto value = g_ptr.load(relaxed)->load(relaxed);
        while (value < 100)
        {
            //assert(value >= 0 && value <= 100);  // the loop condition already checks for <100, what's the point here?  Just the >= 0 part?
            auto tmp_ptr = g_ptr.load(relaxed);  // formally should be consume, but compilers strengthen it unnecessarily.
            value = tmp_ptr->load(relaxed);
            //value = g_ptr.load(relaxed)->load(relaxed);  // or without a tmp var
        }
    }
    

    (戈德博尔特)


    指向的值被原子读取

    是的,因为它是std::atomic<int>。

    它必须从 RAM 中获取

    嗯,来自共享内存,所以实际上可能来自缓存。在所有实际的 C++ 实现中,std::thread 运行的内核之间的缓存都是一致的。(在内核共享内存但不具备缓存一致性的主板上,例如 ARM DSP + 微控制器,您不会在这些内核上运行同一程序的线程。)

    在 C++ 中,正式情况下至少需要std::memory_order_consume,但由于原始定义过于复杂,难以实现,它已被弃用并被移除。编译器只是通过将其提升为 来实现它acquire,而实际上并没有利用除 DEC Alpha 之外所有 ISA 都提供的内存依赖排序。

    在这种情况下,编译器没有合理的方法来确定指针加载必须产生的值,因此必须编写一个汇编程序来加载并解引用该指针。因此,第一个pointer.load(relaxed)和之间存在数据依赖关系tmp->load(relaxed),而真正的ISA保证在这种情况下不会发生LoadLoad重排序。因此它在实践中是有效的;例如,Linux内核将此用于RCU,以避免读取路径中的任何内存屏障。

    参见C++11:memory_order_relaxed 和 memory_order_consume 之间的区别consume以及有关内存依赖排序的类似答案。

    • 8
  2. Homer512
    2025-04-27T05:23:00+08:002025-04-27T05:23:00+08:00

    不,这不安全。g_atomic应该是std::atomic<std::atomic<int>*>。指向原子对象的原子指针。

    即使底层硬件不会撕裂指针大小的读/写,由于指针既不是原子的也不是易失性的,因此编译器可以自由地优化指针值的所有加载/存储,除了最后一个循环中的最后一次写入和读取器中的第一次加载。

    您的读取器可能会无限循环,因为它不断检查相同的旧分配指针而不是选择下一个分配。

    至于存储在的值atomic<int>,标准说:

    初始化不是原子操作 (6.9.2)。[注意:在构造过程中,可能会访问原子对象A竞争,例如,通过对合适的原子指针变量A进行操作,将刚构造的对象的地址传递给另一个线程memory_order::relaxed,然后A在接收线程中立即访问。这会导致未定义的行为。——结束语]

    因此,原子指针应该通过内存获取/释放的顺序来访问。你的栅栏加上一个放松操作也应该可以工作(如果与读取端的栅栏配对),但其开销比实际需要的要大。

    • 3

相关问题

  • 为什么编译器在这里错过矢量化?

  • 使用带有库的 CMake 编译错误[关闭]

  • 每次我尝试运行预制时都会抛出错误

  • 如何在 C++ 中创建类似于 std::byte 的八位字节类型?

  • C++17 中 std::byte 只能按位运算?

Sidebar

Stats

  • 问题 205573
  • 回答 270741
  • 最佳答案 135370
  • 用户 68524
  • 热门
  • 回答
  • Marko Smith

    重新格式化数字,在固定位置插入分隔符

    • 6 个回答
  • Marko Smith

    为什么 C++20 概念会导致循环约束错误,而老式的 SFINAE 不会?

    • 2 个回答
  • Marko Smith

    VScode 自动卸载扩展的问题(Material 主题)

    • 2 个回答
  • Marko Smith

    Vue 3:创建时出错“预期标识符但发现‘导入’”[重复]

    • 1 个回答
  • Marko Smith

    具有指定基础类型但没有枚举器的“枚举类”的用途是什么?

    • 1 个回答
  • Marko Smith

    如何修复未手动导入的模块的 MODULE_NOT_FOUND 错误?

    • 6 个回答
  • Marko Smith

    `(表达式,左值) = 右值` 在 C 或 C++ 中是有效的赋值吗?为什么有些编译器会接受/拒绝它?

    • 3 个回答
  • Marko Smith

    在 C++ 中,一个不执行任何操作的空程序需要 204KB 的堆,但在 C 中则不需要

    • 1 个回答
  • Marko Smith

    PowerBI 目前与 BigQuery 不兼容:Simba 驱动程序与 Windows 更新有关

    • 2 个回答
  • Marko Smith

    AdMob:MobileAds.initialize() - 对于某些设备,“java.lang.Integer 无法转换为 java.lang.String”

    • 1 个回答
  • Martin Hope
    Fantastic Mr Fox msvc std::vector 实现中仅不接受可复制类型 2025-04-23 06:40:49 +0800 CST
  • Martin Hope
    Howard Hinnant 使用 chrono 查找下一个工作日 2025-04-21 08:30:25 +0800 CST
  • Martin Hope
    Fedor 构造函数的成员初始化程序可以包含另一个成员的初始化吗? 2025-04-15 01:01:44 +0800 CST
  • Martin Hope
    Petr Filipský 为什么 C++20 概念会导致循环约束错误,而老式的 SFINAE 不会? 2025-03-23 21:39:40 +0800 CST
  • Martin Hope
    Catskul C++20 是否进行了更改,允许从已知绑定数组“type(&)[N]”转换为未知绑定数组“type(&)[]”? 2025-03-04 06:57:53 +0800 CST
  • Martin Hope
    Stefan Pochmann 为什么 {2,3,10} 和 {x,3,10} (x=2) 的顺序不同? 2025-01-13 23:24:07 +0800 CST
  • Martin Hope
    Chad Feller 在 5.2 版中,bash 条件语句中的 [[ .. ]] 中的分号现在是可选的吗? 2024-10-21 05:50:33 +0800 CST
  • Martin Hope
    Wrench 为什么双破折号 (--) 会导致此 MariaDB 子句评估为 true? 2024-05-05 13:37:20 +0800 CST
  • Martin Hope
    Waket Zheng 为什么 `dict(id=1, **{'id': 2})` 有时会引发 `KeyError: 'id'` 而不是 TypeError? 2024-05-04 14:19:19 +0800 CST
  • Martin Hope
    user924 AdMob:MobileAds.initialize() - 对于某些设备,“java.lang.Integer 无法转换为 java.lang.String” 2024-03-20 03:12:31 +0800 CST

热门标签

python javascript c++ c# java typescript sql reactjs html

Explore

  • 主页
  • 问题
    • 最新
    • 热门
  • 标签
  • 帮助

Footer

AskOverflow.Dev

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve