gpt4 book ai didi

c++ - 在原子多线程代码中删除容器

转载 作者:行者123 更新时间:2023-12-02 01:19:42 27 4
gpt4 key购买 nike

考虑以下代码:

struct T { std::atomic<int> a = 2; };
T* t = new T();
// Thread 1
if(t->a.fetch_sub(1,std::memory_order_relaxed) == 1)
delete t;
// Thread 2
if(t->a.fetch_sub(1,std::memory_order_relaxed) == 1)
delete t;

我们确切地知道线程 1 和线程 2 之一将执行 delete .但是我们安全吗?我的意思是假设线程 1 将执行 delete .是否保证当线程 1 开始 delete , 线程 2 甚至不会读取 t ?

最佳答案

  • 让调用操作t->a.fetch_sub(1,std::memory_order_relaxed)Release
  • Releasea 的原子修改
  • All modifications to any particular atomic variable occur in atotal order that is specific to this one atomic variable.
  • 全部Release以总顺序发生
  • Thread 1Release先到先得 Thread 2Release在它之后
  • 所以Thread 1查看值 2 并且因为 2 != 1 只是退出而不是
    不再访问
  • Thread 2查看值 1 并且因为 1 == 1 调用 delete t

  • 请注意拨打 delete发生在 Release 之后在 Thread 2ReleaseThread 2发生在 Release 之后在 Thread 1
    打电话 deleteThread 2发生在 Release 之后在 Thread 1Release 之后不再访问 t

    但在现实生活中(不是在这个具体的例子中)通常我们需要使用 memory_order_acq_rel相反 memory_order_relaxed .

    这是因为真实的对象通常有更多的数据字段,而不仅仅是原子引用计数。

    线程可以写入/修改对象中的一些数据。从另一方面 - 在析构函数内部,我们需要查看其他线程所做的所有修改。

    因为这不是最后一个版本必须有 memory_order_release语义。最后 Release必须有 memory_order_acquire在这一切修改后查看。举个例子
    #include <atomic>

    struct T {
    std::atomic<int> a;
    char* p;

    void Release() {
    if(a.fetch_sub(1,std::memory_order_acq_rel) == 1) delete this;
    }

    T()
    {
    a = 2, p = nullptr;
    }

    ~T()
    {
    if (p) delete [] p;
    }
    };

    // thread 1 execute
    void fn_1(T* t)
    {
    t->p = new char[16];
    t->Release();
    }

    // thread 2 execute
    void fn_2(T* t)
    {
    t->Release();
    }

    在析构函数中 ~T()我们必须查看 t->p = new char[16]; 的结果即使析构函数将在线程 2 中调用。如果使用 memory_order_relaxed正式这不能保证。
    但使用 memory_order_acq_rel

    最后的线程 Release , 将使用 memory_order_acquire 执行语义也是(因为 memory_order_acq_rel 包含它)将是 t->p = new char[16]; 的查看结果操作,因为它发生在同一个 a 上的另一个原子操作之前变量与 memory_order_release语义(因为 memory_order_acq_rel 包括它)

    因为仍然存在疑问,我试着再做一个证明

    给出:
    struct T { 
    std::atomic<int> a;

    T(int N) : a(N) {}

    void Release() {
    if (a.fetch_sub(1,std::memory_order_relaxed) == 1) delete this;
    }
    };
  • 让 a 初始化为 N (=1,2,...∞)
  • 让 Release() 准确调用 N 次

  • 问题:代码是否正确且 电话 会被删除吗?

    N = 1 - 所以 a == 1开始和 Release()叫过一次。

    这里存在问题?有人说这是“UB”? (在 a 之后访问 delete this 开始执行还是如何?!)
    delete this直到 a.fetch_sub(1,std::memory_order_relaxed) 才能开始执行将被计算,因为 delete this 取决于结果 a.fetch_sub .编译器或 cpu 无法重新排序 delete this之前 a.fetch_sub(1,std::memory_order_relaxed)完成的。

    因为 a == 1 - a.fetch_sub(1,std::memory_order_relaxed)返回 1, 1 == 1所以 delete this将被调用。

    以及对 delete this 之前的对象的所有访问权限开始执行。

    所以代码正确和 T删除案例 N == 1 .

    现在让我们以防万一 N == n全都正确。所以找案例 N = n + 1. (n = 1,2..∞)
  • a.fetch_sub是原子变量的修改。
  • 对任何特定原子变量的所有修改都发生在总计
    特定于这个原子变量的顺序。
  • 所以我们可以说一些 a.fetch_sub将被执行 第一 (在
    修改顺序 )
  • 第一 (按修改顺序 a )a.fetch_sub返回n + 1 != 1 (n = 1..∞) - 所以 Release()其中将执行此
    第一 a.fetch_sub , 不打电话退出 delete this
  • delete this 还没有叫 - 只会被调用
    a.fetch_sub返回 1,但是这个 a.fetch_sub将被称为 后第一 a.fetch_sub
  • 并将是 a == n第一 a.fetch_sub完成(这个
    之前 所有其他 n a.fetch_sub )
  • 所以一个Release (其中 第一个 a.fetch_sub 执行)退出
    没有 delete this并完成访问对象之前 delete this开始
  • 我们现在有 n休息 Release()电话和 a == n在任何之前a.fetch_sub ,但这种情况已经可以了


  • 对于那些认为代码不安全/UB 的人来说,还有一个注意事项。

    只有当我们在对象的任何访问完成之前开始删除时,才可能不安全。

    但删除只会在 a.fetch_sub 之后返回 1。

    这意味着另一个 a.fetch_sub已经修改 a
    因为 a.fetch_sub是原子的 - 如果我们查看它的副作用(修改 a ) - a.fetch_sub - 不再访问 a
    真的,如果操作将值写入内存位置( a ),然后再次访问该内存 - 这已经不是原子意义上的了。

    所以如果我们查看原子修改的结果 - 它已经完成并且没有更多的访问变量

    结果删除将已经在所有访问 a之后完全的。

    并且这里不需要任何特殊的原子内存顺序(relaxed,acq,rel)。即使是轻松的订单也可以。我们只需要操作的原子性。
    memory_order_acq_rel需要如果对象 T 不仅包含 a柜台。我们希望在析构函数中查看对 T 的另一个字段的所有内存修改

    关于c++ - 在原子多线程代码中删除容器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59537415/

    27 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com