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

Koo*_*sha 6 c++ multithreading atomic stdatomic

考虑以下代码:

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;
Run Code Online (Sandbox Code Playgroud)

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

RbM*_*bMm 6

请注意,调用delete发生在Releasein之后Thread 2ReleaseinThread 2发生在Releasein之后Thread 1

所以通话deleteThread 2情况后,ReleaseThread 1 其未获得牛逼了之后Release

但在一般的真实生活(而不是在这个具体的例子),我们需要使用memory_order_acq_rel代替memory_order_relaxed

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

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

因为这不是最后一个 Release 必须有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();
}
Run Code Online (Sandbox Code Playgroud)

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

final 之后的线程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;
    }
};
Run Code Online (Sandbox Code Playgroud)
  • 让 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。编译器或cpudelete thisa.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.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 的另一个字段的所有内存修改

  • 好的,我知道了。我没有对`T::a`的值给予足够的重视,恰好先运行的线程永远无法删除`t`。 (2认同)