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?
让调用操作 t->a.fetch_sub(1,std::memory_order_relaxed)
Release
Release 是原子修饰 a
Release发生在一个总的顺序Thread 1做Release第一,比Thread 2做Release
后Thread 1查看值 2 并且因为 2 != 1 只是退出并且不再访问 tThread 2 查看值 1 并且因为 1 == 1 调用 delete t请注意,调用delete发生在Releasein之后Thread 2,
ReleaseinThread 2发生在Releasein之后Thread 1
所以通话delete的Thread 2情况后,Release在Thread 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)
问题:代码是否正确,T是否会被删除?
让N = 1- 所以一a == 1开始Release()就叫了一次。
这里存在问题?有人说这是“UB”?(a在delete this开始执行后访问或如何访问?!)
delete this不能开始执行,直到a.fetch_sub(1,std::memory_order_relaxed)将被计算,因为delete this 从结果依赖的a.fetch_sub。编译器或cpudelete 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.fetch_sub返回
n + 1 != 1 (n = 1..?)-所以Release()在将要执行该
第一 a.fetch_sub,退出而不呼叫delete thisdelete this 尚未调用- 只有在 a.fetch_sub返回 1之后才会调用
它,但这a.fetch_sub将在第一个之后调用 a.fetch_suba == 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 的另一个字段的所有内存修改