memory_order_acquire 上的引用计数指针是否多余?
Is memory_order_acquire on reference counted pointers to trivially destructible types superfluous?
这个问题专门针对引用计数指针中的普通可破坏类型。请参阅 Boost's documentation 中关于原子的使用的示例。
减量如下:
if (x->refcount_.fetch_sub(1, boost::memory_order_release) == 1) {
// A
boost::atomic_thread_fence(boost::memory_order_acquire);
delete x;
}
我们知道,由于 memory_order_release
,x
的所有 reads/writes 都在 fetch_sub
之前完成(参见 here ).因此,如果我们碰巧到达 A
点,那么 x
的所有使用都已完成。
在代码中的 A
点,标准不保证我们在 memory_order_acquire
栅栏之后看到 x
的最新值。 .
所以这是我关于 memory_order_acquire
的第二个陈述的问题:
当 x
指向一个普通的可破坏类型(例如,int
,其中 x
是 int * const
)时 memory_order_acquire
毫无意义吗?我的理由是,如果 x
是微不足道的可破坏的,那么对 x
的最新更改不会影响删除 x
?
例如删除线程的delete x;
是否看到最新的x
使得*x = 10
或过时的值使得*x = 8
销毁过程总是相同的不管怎样(只要指针 x
本身保持不变)。由于发布,它知道从那时起没有人会修改 x
,因此它所要做的就是解除分配。
memory_order_acquire
还有什么我在这里遗漏的好处吗?
我的想法是否正确,如果不正确,为什么我们需要在删除线程上查看 x
的最新值?
不行!
您似乎认为障碍是一种能够
- 在线程中发布数据结构,通常在原子指针上带有 release/store(类似于 Java 中的易失性引用)
- 检查发布是否发生(加载原子指针并检查值)并读取数据(load/acquire)
但这只是使用原子和屏障的一个例子。
一般来说,与宽松的原子操作相关的障碍使得互斥定义明确。 mutex 是互斥设备,null then non null 原子指针是另一个,引用计数是另一个。
引用计数的功能类似于 RW 锁,具有:
- RC增量=R锁
- RC减量=R解锁
- 减量后观察(RC = 0) = W lock
(RC=0)观察是锁操作的类比,因为它必须与属性(RC>0)互斥。对于需要排除的每一系列计算,互斥转化为释放-获取对。 RC设备控制的数据的所有使用者都需要与内存释放(不是互斥锁释放)操作互斥。
让我们考虑以下示例:
初始化
int * const x = new int{42};
std::atomic<int> refcount = 2;
线程 A 和线程 B
assert(*x == 42);
if (refcount.fetch_sub(1, std::memory_order_release) == 1) {
// std::atomic_thread_fence(std::memory_order_acquire);
delete x;
}
在该示例中,断言可能会失败或更糟,因为它可能会访问已销毁和释放的对象。问题是在一个线程中访问 *x
和在另一个线程中删除之间没有 happens before 关系。即使在使用 memory_order_release
时,在同一线程中 refcount
递减后,断言也可以重新排序。
为了形成这种发生在之前的关系,我们需要线程之间的同步点,而release-acquire正是这样做的。这就是为什么我们在删除 *x
之前需要一个获取栅栏。或者,我们可以在递减 refcount
时使用 memory_order_acq_rel
而不是 memory_order_release
,这也足够了。
标准是而不是,它是根据可能发生的线程中的操作交错编写的。我收集这样的规范太严格了:编译器需要重新排序加载和存储,甚至跨同步点,以提高速度。
相反,该标准只是说明了什么是数据竞争,并且它们是未定义的行为。
非正式地,a data race occurs when:
- 一个线程访问内存;
- 另一个线程写入相同的内存位置;和
- 两个线程之间没有同步来强加一个
两次访问的顺序。
删除一个对象算作一次写入,即使析构函数是微不足道的。如果两个线程访问 *x
然后使用您的代码减少它,并且一个线程删除 x
,我们可以看到所有三个要求都已满足。 *x
.
上存在数据竞争
数据竞争不在 x->refcount_
,因为两个线程都通过原子操作访问它(在标准中,我上面给出的“数据竞争”的草率定义是一个明确的例外)。但是因为两个线程上的内存顺序都是release
,所以它不会同步两个线程。
人们经常试图想象哪些编译器恶作剧可能会在实践中导致实际的不当行为,看看这种竞争是否被认为是“良性的”,但我已经放弃了。根据标准,缺少 release-acquire 切换使得行为未定义。
这个问题专门针对引用计数指针中的普通可破坏类型。请参阅 Boost's documentation 中关于原子的使用的示例。
减量如下:
if (x->refcount_.fetch_sub(1, boost::memory_order_release) == 1) {
// A
boost::atomic_thread_fence(boost::memory_order_acquire);
delete x;
}
我们知道,由于
memory_order_release
,x
的所有 reads/writes 都在fetch_sub
之前完成(参见 here ).因此,如果我们碰巧到达A
点,那么x
的所有使用都已完成。在代码中的
A
点,标准不保证我们在memory_order_acquire
栅栏之后看到x
的最新值。 .
所以这是我关于 memory_order_acquire
的第二个陈述的问题:
当 x
指向一个普通的可破坏类型(例如,int
,其中 x
是 int * const
)时 memory_order_acquire
毫无意义吗?我的理由是,如果 x
是微不足道的可破坏的,那么对 x
的最新更改不会影响删除 x
?
例如删除线程的delete x;
是否看到最新的x
使得*x = 10
或过时的值使得*x = 8
销毁过程总是相同的不管怎样(只要指针 x
本身保持不变)。由于发布,它知道从那时起没有人会修改 x
,因此它所要做的就是解除分配。
memory_order_acquire
还有什么我在这里遗漏的好处吗?
我的想法是否正确,如果不正确,为什么我们需要在删除线程上查看 x
的最新值?
不行!
您似乎认为障碍是一种能够
- 在线程中发布数据结构,通常在原子指针上带有 release/store(类似于 Java 中的易失性引用)
- 检查发布是否发生(加载原子指针并检查值)并读取数据(load/acquire)
但这只是使用原子和屏障的一个例子。
一般来说,与宽松的原子操作相关的障碍使得互斥定义明确。 mutex 是互斥设备,null then non null 原子指针是另一个,引用计数是另一个。
引用计数的功能类似于 RW 锁,具有:
- RC增量=R锁
- RC减量=R解锁
- 减量后观察(RC = 0) = W lock
(RC=0)观察是锁操作的类比,因为它必须与属性(RC>0)互斥。对于需要排除的每一系列计算,互斥转化为释放-获取对。 RC设备控制的数据的所有使用者都需要与内存释放(不是互斥锁释放)操作互斥。
让我们考虑以下示例:
初始化
int * const x = new int{42};
std::atomic<int> refcount = 2;
线程 A 和线程 B
assert(*x == 42);
if (refcount.fetch_sub(1, std::memory_order_release) == 1) {
// std::atomic_thread_fence(std::memory_order_acquire);
delete x;
}
在该示例中,断言可能会失败或更糟,因为它可能会访问已销毁和释放的对象。问题是在一个线程中访问 *x
和在另一个线程中删除之间没有 happens before 关系。即使在使用 memory_order_release
时,在同一线程中 refcount
递减后,断言也可以重新排序。
为了形成这种发生在之前的关系,我们需要线程之间的同步点,而release-acquire正是这样做的。这就是为什么我们在删除 *x
之前需要一个获取栅栏。或者,我们可以在递减 refcount
时使用 memory_order_acq_rel
而不是 memory_order_release
,这也足够了。
标准是而不是,它是根据可能发生的线程中的操作交错编写的。我收集这样的规范太严格了:编译器需要重新排序加载和存储,甚至跨同步点,以提高速度。
相反,该标准只是说明了什么是数据竞争,并且它们是未定义的行为。
非正式地,a data race occurs when:
- 一个线程访问内存;
- 另一个线程写入相同的内存位置;和
- 两个线程之间没有同步来强加一个 两次访问的顺序。
删除一个对象算作一次写入,即使析构函数是微不足道的。如果两个线程访问 *x
然后使用您的代码减少它,并且一个线程删除 x
,我们可以看到所有三个要求都已满足。 *x
.
数据竞争不在 x->refcount_
,因为两个线程都通过原子操作访问它(在标准中,我上面给出的“数据竞争”的草率定义是一个明确的例外)。但是因为两个线程上的内存顺序都是release
,所以它不会同步两个线程。
人们经常试图想象哪些编译器恶作剧可能会在实践中导致实际的不当行为,看看这种竞争是否被认为是“良性的”,但我已经放弃了。根据标准,缺少 release-acquire 切换使得行为未定义。