V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
cnbatch
V2EX  ›  C++

atomic<shared_ptr<T>>在 GCC 和 Clang 的受支持程度真是一言难尽

  •  
  •   cnbatch · 19 天前 · 1634 次点击

    Clang 到现在都不支持atomic<shared_ptr<T>>,只能继续 atomic_load()atomic_store()。一旦要用 weak_ptr 则如同残废,不支持atomic_load()atomic_store()

    GCC 12.2 及旧版本有“bug”( P0718R2 的疏忽),刚好 Debian 12 自带的 GCC 就是 12.2 ,直接完蛋。

    这段代码在 Debian 12 (bookworm)无法编译,换成 Debian Testing (trixie)就可以成功编译:

    #include <atomic>
    #include <memory>
    
    class A{ int a; };
    
    int main()
    {
        std::atomic<std::shared_ptr<A>> a_ptr = std::make_shared<A>();
        a_ptr = nullptr;
    
        return 0;
    }
    

    想要写跨编译器的代码很麻烦,只能加好几行#if #else #endif

    Clang 原本已经有人在实现atomic<shared_ptr<T>>,准备到一半就放弃了:
    [libc++] Implement P0718R2: atomic<shared_ptr<T>>
    放弃的原因令人无语,因为 PR 内wait/notify_all的效率不太高,需要重写,作者直接不干了

    18 条回复    2024-10-21 16:47:42 +08:00
    Coelacanthus
        1
    Coelacanthus  
       19 天前
    GCC 12 分支最新的 fix 版本是 12.4 啊,Debian 他们居然没更新。你可以给他们提个 request 要求更新,理由就是有已知 bug 。
    felixlong
        2
    felixlong  
       19 天前
    把 shared_ptr 放到 atomic 里面的使用场景是什么?
    cnbatch
        3
    cnbatch  
    OP
       19 天前
    @Coelacanthus 刚用 reportbug 命令向 Debian 提交了,暂时还没什么反应,就连编号都还没生成。
    不太熟悉他们的流程,看来只能继续等待
    cnbatch
        4
    cnbatch  
    OP
       19 天前
    @felixlong 多线程读写会用到,放进 atomic 就不需要再用 mutex 了。

    举个例子,atomic<shared_ptr<Description>>,用来保存一段文字描述,修改时直接 make_shared<Description>,其他线程读取时调用 load() 成员函数或者用 std::atomic_load(),可以确保修改与读取互不干扰。对于时间敏感度不高的场景会很方便,起码不会死锁。
    flax5a98aa2
        5
    flax5a98aa2  
       19 天前
    https://en.cppreference.com/w/cpp/header/hazard_pointer 这个是不是更有盼头一点?
    flax5a98aa2
        6
    flax5a98aa2  
       19 天前
    [Lock-free Atomic Shared Pointers Without a Split Reference Count? It Can Be Done!](
    ) 还有这个,我以前看没看懂,看到这个帖子想起来了,不知道是否对你有帮助
    bruce0
        7
    bruce0  
       18 天前
    同一个编译器, clang, 我记得 Mac 上的 clang 现在还不支持 std::jthread 呢(上半年还不支持,不知道现在支持了吗), linux 的 clang 是支持的
    newma
        8
    newma  
       18 天前
    @cnbatch 这个例子里面用不用 atomic 都没关系吧,make_shared 都另外 new 一个新对象了,返回一个全新的 shared_ptr 和全新的 Description 内存,这个过程和原来的那个 shared_ptr<Description>对象都没关系啊。
    minami
        9
    minami  
       18 天前
    msvc: 我看看现在还有没有黑子来黑我
    changnet
        10
    changnet  
       18 天前
    首先 C++12 并不完全支持 C++20 ,要在 C++12 上使用 C++20 风险就比较大,因为用着用着就不知道哪个特性不支持,或者有 bug 。所以生产环境一般都不用这么新的标准

    你这种情况,一般是自己用 SpinLock 之类的再实现一个类型来替代 std::atomic<std::shared_ptr<A>>

    另外 std::atomic<std::shared_ptr<A>>只是对 std::shared_ptr 保证原子操作,对 A 里的数据操作并不是原子的。而 std::shared_ptr 本身的几个函数是线程安全的,你不修改 shared_ptr 本身的话 std::atomic 没啥意义。所以你这个需求还是比较小众的
    changnet
        11
    changnet  
       18 天前
    @changnet GCC12 ,不是 C++12
    cnbatch
        12
    cnbatch  
    OP
       18 天前
    @bruce0 刚看了下 cppreference ,显示还没支持,Apple Clang 总是慢一拍吧
    cnbatch
        13
    cnbatch  
    OP
       18 天前
    @newma
    @changnet cppreference 是这么说的:
    https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic

    If multiple threads of execution access the same std::shared_ptr object without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur unless all such access is performed through these functions, which are overloads of the corresponding atomic access functions (std::atomic_load, std::atomic_store, etc.).

    如果多个执行线程无同步地访问同一 std::shared_ptr 对象,而其中由任何访问使用了 shared_ptr 的非 const 成员函数,那么就会发生数据竞争,除非所有这种访问都通过这些作为对应原子访问函数( std::atomic_load 、std::atomic_store 等)重载的函数进行。

    non-const 的成员函数是:
    reset()、swap()、operator=

    我的例子刚好就是一边换指针、一边读指针,按照 cppreference 的说法,应当使用 atmoic 操作。
    cnbatch
        14
    cnbatch  
    OP
       18 天前
    @minami 说实话,如果 MSVC 支持多系统,全用 MSVC 也不是坏事
    newma
        15
    newma  
       18 天前
    @cnbatch 兄弟,你的例子里面没有换指针。make_shared 是重新构造一个全新的 shared_ptr ,和原来的 share_ptr 没任何联系的,这 2 个 shared_ptr 所指的对象也没任何联系
    cnbatch
        16
    cnbatch  
    OP
       18 天前
    @newma
    有关联的是 atomic<shared_ptr<T>>变量本身,不是 shared_ptr 之间

    如果不用 atomic ,那就:

    共享变量:
    shared_ptr<Description> share_desc_ptr;

    线程 1:
    shared_ptr<Description> desc_ptr = share_desc_ptr;

    线程 2:
    share_desc_ptr = make_shared_ptr<Description>();

    这里用到了 operator=,按照 cppreference 的描述,会发生数据竞争。所以要改成:


    C++20 之前:

    共享变量:
    shared_ptr<Description> share_desc_ptr;

    线程 1:
    shared_ptr<Description> desc_ptr = atomic_load(&share_desc_ptr);

    线程 2:
    atomic_store(&share_desc_ptr, make_shared_ptr<Description>());


    C++20 起:

    共享变量:
    atomic<shared_ptr<Description>> share_desc_ptr;

    线程 1:
    shared_ptr<Description> desc_ptr = atomic_load(&share_desc_ptr);
    // 或者 shared_ptr<Description> desc_ptr = share_desc_ptr; 自动调用 atomic 的 operator=

    线程 2:
    atomic_store(&share_desc_ptr, make_shared_ptr<Description>());
    // 或者 share_desc_ptr = make_shared_ptr<Description>(); 同样自动调用 atomic 的 operator=
    newma
        17
    newma  
       18 天前
    @cnbatch 哦哦,你是原地修改相同的变量啊。看来是我误会了,我以为是原地有个 shared_ptr ,多个线程读,如果有某个线程需要修改,则 make_shared 进行分离再修改自身。你这个不同线程修改同一个东西确实需要原子或者上锁。
    tinykey
        18
    tinykey  
       18 天前
    @cnbatch 不是因为这个原因。atomic<shared_ptr>解决的是 shared_ptr 的引用计数和 object 分开操作的问题。
    例如,当一个线程 A 引用计数减一的时候,其他线程 B 可能正在引用计数加一.但线程 A 可能减一后直接析构这个 object 。
    这样线程 B 就会 use after free.

    目前有两种解决办法:
    一种是将引用计数和 object 的地址用一个指针表示。例如,虚拟地址是 64bit ,但是 intel 只用到了 48bit ,这样高位 16 bit 就可以用于表示引用计数。这样只需要操作同一个变量就可以实现引用计数和 object 同时操作。
    另外一种就是类似在 c++中引入 GC ,延迟删除引用计数为 0 的 object 。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2575 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 15:31 · PVG 23:31 · LAX 07:31 · JFK 10:31
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.