在原子多线程代码中删除容器
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
不会被删除。
让调用操作t->a.fetch_sub(1,std::memory_order_relaxed)
Release
Release
是 a
的原子修改
- 对任何特定原子变量的所有修改都发生在
特定于这个原子变量的总顺序。
- 因此所有
Release
都按总顺序排列
- 让
Thread 1
先做Release
然后Thread 2
做Release
在它之后
- 所以
Thread 1
查看值 2 因为 2 != 1 只是退出而不是
再访问 t
Thread 2
查看值 1 并且因为 1 == 1 调用 delete 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" ? (a
在 delete 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
开始执行之前对对象的所有访问。
所以代码正确并且 T
在 N == 1
的情况下被删除。
现在假设 N == n
全部正确。所以寻找案例 N = n + 1. (n = 1,2..∞)
a.fetch_sub
是修改原子变量。
- 对任何特定原子变量的所有修改总共发生在
特定于这个原子变量的顺序。
- 所以我们可以说一些
a.fetch_sub
将被执行 first (在
修改顺序 a)
- this first(按修改顺序a)
a.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
的另一个字段的所有内存修改
考虑以下代码:
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 thedelete
, Thread 2 won't even readt
?
是的,为了让线程 1 删除 t
,第二个线程中减少值的读取必须已经发生,否则 if
语句不会评估为 true 和 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 只是退出而不是 再访问 t Thread 2
查看值 1 并且因为 1 == 1 调用delete t
请注意,调用 delete
发生在 Thread 2
中的 Release
之后,并且
Release
in Thread 2
发生在 Release
in Thread 1
所以在 Thread 2
中调用 delete
发生在 Thread 1
中的 Release
之后
在 Release
但在现实生活中(不是在这个具体例子中)一般我们需要使用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" ? (a
在 delete 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
开始执行之前对对象的所有访问。
所以代码正确并且 T
在 N == 1
的情况下被删除。
现在假设 N == n
全部正确。所以寻找案例 N = n + 1. (n = 1,2..∞)
a.fetch_sub
是修改原子变量。- 对任何特定原子变量的所有修改总共发生在 特定于这个原子变量的顺序。
- 所以我们可以说一些
a.fetch_sub
将被执行 first (在 修改顺序 a) - this first(按修改顺序a)
a.fetch_sub
returnn + 1 != 1 (n = 1..∞)
- 所以Release()
将在其中执行 先a.fetch_sub
,不调用就退出delete this
- 和
delete this
还没有被调用 - 它只会被调用 aftera.fetch_sub
which return 1, but thisa.fetch_sub
will be called after firsta.fetch_sub
- 并且在第
a.fetch_sub
完成后a == n
(这 将 在 所有其他n
a.fetch_sub
) 之前
- so one
Release
(where firsta.fetch_sub
executed ) exit 没有delete this
并且它完成访问对象 beforedelete 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