c++ - 在原子多线程代码中删除容器
问题描述
考虑以下代码:
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
Release
是原子修饰a
- 对任何特定原子变量的所有修改都以特定于该原子变量的总顺序发生。
- 所以一切都
Release
以总顺序发生 - 先做
后
Thread 1
做Release
_Thread 2
_Release
- 所以
Thread 1
查看值 2 并且因为 2 != 1 只是退出并且不再访问 t Thread 2
查看值 1 并且因为 1 == 1 调用delete t
请注意,调用delete
发生在Release
in之后Thread 2
,
Release
inThread 2
发生在Release
in之后Thread 1
所以调用delete
发生在Thread 2
之后
不再访问 t 之后Release
Thread 1
Release
但在现实生活中(不是在这个具体例子中)一般我们需要 usememory_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();
}
在析构函数中,即使在线程 2 中调用析构函数~T()
,我们也必须查看结果。如果使用正式,则不能保证。但使用 memory_order_acq_relt->p = new char[16];
memory_order_relaxed
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;
}
};
- 让 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
取决于. 编译器或 cpu 在完成之前a.fetch_sub
无法重新排序。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) - this first(按修改顺序a)
a.fetch_sub
returnn + 1 != 1 (n = 1..∞)
- soRelease()
将首先执行 this ,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 的另一个字段的所有内存修改
推荐阅读
- python - Python websocket客户端 - 确定连接是否打开的方法
- python - 你能在 Python 中检索函数的有效参数列表吗
- selenium - 使用 selenium 访问网站时无法与客户端建立 TLS
- blazor-webassembly - 如何从 Blazor WebAssembly 中的 sessionStorage 读取大字符串?我的结果被截断
- c++ - 如何使用支持多个文件的 arduino 创建 Web 服务器?
- sql - 查询以替换 PostgreSQL 中 int 数组中的部分值
- java - Java 程序未显示所需的输出
- swift - PreviewProvider 在小部件扩展中不起作用
- python - ModuleNotFoundError:没有名为“alpha_vantage”的模块
- javascript - 如何在javascript中获取Json Serialize的值?