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

Deleting the container in atomic multi-threaded code

考虑以下代码:

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 不会被删除直到两个线程都有已经读取指针。尽管我仍然强烈建议使用 std::shared_ptr 如果您想使用引用计数管理指针的生命周期而不是尝试自己做。这就是它的用途。

suppose Thread 1 will execute the delete. Is it guaranteed that when Thread 1 started the delete, Thread 2 won't even read t?

是的,为了让线程 1 删除 t,第二个线程中减少值的读取必须已经发生,否则 if 语句不会评估为 true 和 t 不会被删除。

请注意,调用 delete 发生在 Thread 2 中的 Release 之后,并且 Release in Thread 2 发生在 Release in Thread 1

之后

所以在 Thread 2 中调用 delete 发生在 Thread 1 中的 Release 之后 在 Release

之后不再访问 t

但在现实生活中(不是在这个具体例子中)一般我们需要使用memory_order_acq_rel而不是memory_order_relaxed

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

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

因为这不是最后一个版本必须具有 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();
}

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

thread 在 final Release 之后,也将使用 memory_order_acquire 语义执行(因为 memory_order_acq_rel 包含它)将是 t->p = new char[16]; 操作的视图结果,因为它发生了在对具有 memory_order_release 语义的同一个 a 变量进行另一个原子操作之前(因为 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 次

问题:代码是否正确,T 是否会被删除?

N = 1 - 所以 a == 1 在开始时 Release() 调用一次。

这里有问题吗?有人说这是 "UB" ? (adelete this 开始执行后访问或如何?!)

delete this 在计算出 a.fetch_sub(1,std::memory_order_relaxed) 之前无法开始执行,因为 delete this 取决于 a.fetch_sub 的结果 。编译器或 cpu 无法在 a.fetch_sub(1,std::memory_order_relaxed) 完成之前对 delete this 重新排序。

因为a == 1-a.fetch_sub(1,std::memory_order_relaxed)return1,1 == 1所以delete this会被调用

并且在 delete this 开始执行之前对对象的所有访问。

所以代码正确并且 TN == 1 的情况下被删除。

现在假设 N == n 全部正确。所以寻找案例 N = n + 1. (n = 1,2..∞)

  • a.fetch_sub是修改原子变量。
  • 对任何特定原子变量的所有修改总共发生在 特定于这个原子变量的顺序。
  • 所以我们可以说一些 a.fetch_sub 将被执行 first (在 修改顺序 a)
  • this first(按修改顺序aa.fetch_sub return n + 1 != 1 (n = 1..∞) - 所以 Release() 将在其中执行 a.fetch_sub,不调用就退出delete this
  • delete this 还没有被调用 - 它只会被调用 after a.fetch_sub which return 1, but this a.fetch_sub will be called after first a.fetch_sub
  • 并且在a.fetch_sub完成后a == n(这 将 所有其他 n a.fetch_sub)
  • 之前
  • so one Release (where first a.fetch_sub executed ) exit 没有 delete this 并且它完成访问对象 before delete this start
  • 我们现在有 n 休息 Release() 电话和 a == n a.fetch_sub,但这个案例已经OK了

对于那些认为代码不安全/UB 的人,请注意。

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

但删除只会在 a.fetch_sub return 1.

之后

这意味着另一个a.fetch_sub已经修改了a

因为 a.fetch_sub 是原子的 - 如果我们查看它的副作用(a 的修改) - a.fetch_sub - 不再访问 a

实际上,如果操作将值写入内存位置 (a),然后再次访问此内存 - 这在意义上已经不是原子的。

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

因为对 a 的所有访问都已完成,因此删除已经完成。

这里不需要任何特殊的原子内存顺序(松弛、acq、rel)。即使是放松的顺序也可以。我们只需要操作的原子性。

memory_order_acq_rel 需要如果对象 T 不仅包含 a 计数器。我们希望在析构函数中查看对 T

的另一个字段的所有内存修改