我在crossbeam
中遇到了内存回收问题。假设您正在实现一个包含单个值的简单线程安全无锁容器。任何线程都可以获取存储值的克隆,并且值可以在任何时候更新,之后读者开始观察新值的克隆。
虽然典型的用例是将Arc<X>
指定为T,但实现不能依赖于T指针大小 - 例如,X
可能是一个特征,导致胖-pointer Arc<X>
。但是对任意T的无锁访问似乎非常适合epoch-based lock-free code。根据这些例子,我想出了这个:
extern crate crossbeam;
use std::thread;
use std::sync::atomic::Ordering;
use crossbeam::epoch::{self, Atomic, Owned};
struct Container<T: Clone> {
current: Atomic<T>,
}
impl<T: Clone> Container<T> {
fn new(initial: T) -> Container<T> {
Container { current: Atomic::new(initial) }
}
fn set_current(&self, new: T) {
let guard = epoch::pin();
let prev = self.current.swap(Some(Owned::new(new)),
Ordering::AcqRel, &guard);
if let Some(prev) = prev {
unsafe {
// once swap has propagated, *PREV will no longer
// be observable
//drop(::std::ptr::read(*prev));
guard.unlinked(prev);
}
}
}
fn get_current(&self) -> T {
let guard = epoch::pin();
// clone the latest visible value
(*self.current.load(Ordering::Acquire, &guard).unwrap()).clone()
}
}
与不分配的类型一起使用时,例如使用T=u64
,效果很好 - set_current
和get_current
可以被调用数百万次而不泄漏。 (过程监视器显示由于epoch
伪gc导致的微小内存振荡,如预期的那样,但没有长期增长。)但是,当T是一种分配类型时,例如Box<u64>
,人们很容易发现泄漏。例如:
fn main() {
use std::sync::Arc;
let c = Arc::new(Container::new(Box::new(0)));
const ITERS: u64 = 100_000_000;
let producer = thread::spawn({
let c = Arc::clone(&c);
move || {
for i in 0..ITERS {
c.set_current(Box::new(i));
}
}
});
let consumers: Vec<_> = (0..16).map(|_| {
let c = Arc::clone(&c);
thread::spawn(move || {
let mut last = 0;
loop {
let current = c.get_current();
if *current == ITERS - 1 {
break;
}
assert!(*current >= last);
last = *current;
}
})}).collect();
producer.join().unwrap();
for x in consumers {
x.join().unwrap();
}
}
运行此程序会显示内存使用量稳定且显着增加,最终会消耗与迭代次数成比例的内存量。
根据the blog post introducing it,Crossbeam的epoch回收&#34;不运行析构函数,而只是释放内存&#34;。 Treiber堆栈示例中的try_pop
使用ptr::read(&(*head).data)
将head.data
中包含的值移出目标为释放的head
对象。数据对象的所有权将传输给调用者,调用者将其移动到其他位置或在超出范围时释放它。
如何转换为上面的代码? setter是guard.unlinked
的正确位置,还是确保drop
在底层对象上运行的其他方式?取消注释显式drop(ptr::read(*prev))
会导致断言失败,检查单调性,可能表示过早释放。
答案 0 :(得分:6)
问题的症结在于(正如您已经想到的那样)guard.unlinked(prev)
推迟执行以下代码:
drop(Vec::from_raw_parts(prev.as_raw(), 0, 1));
但是你希望它推迟这个:
drop(Vec::from_raw_parts(prev.as_raw(), 1, 1));
或等同地:
drop(Box::from_raw(prev.as_raw());
换句话说,unlinked
只是释放存储对象的内存,但不会丢弃对象本身。
目前这是Crossbeam的一个众所周知的痛点,但幸运的是它很快就会得到解决。 Crossbeam的基于纪元的垃圾收集器目前正在进行重新设计和重写,以便:
如果您想了解有关新Crossbeam设计的更多信息,请查看RFCs存储库。我建议从RFC on new Atomic和RFC on new GC开始。
我创造了一个实验箱,Coco,与Crossbeam的新设计有很多共同之处。如果您现在需要解决方案,我建议您切换到它。但请记住,一旦我们发布新版本(可能在本月或下个月),Coco将被弃用而不赞成Crossbeam。
答案 1 :(得分:2)
作为Stjepan answered的一些细节,当前Crossbeam的一个已知限制是它只支持解除分配而不是完全丢弃已变得无法访问但仍可能对其他线程可见的对象。这不会影响Crossbeam支持的无锁集合,后者会自动删除集合用户“观察”的项目 - 不允许偷看。这符合队列或堆栈的需要,但不符合例如无锁地图。
这由coco crate解决,它定义了几个并发集合,并作为下一代Crossbeam设计的预览。它支持延迟删除值。以下是Container
使用coco:
use std::thread;
use std::sync::atomic::Ordering;
use coco::epoch::{self, Atomic, Owned};
struct Container<T: Clone> {
current: Atomic<T>,
}
impl<T: Clone> Container<T> {
fn new(initial: T) -> Container<T> {
Container { current: Atomic::new(initial) }
}
fn set_current(&self, new: T) {
epoch::pin(|scope| {
let prev = self.current.swap(Owned::new(new).into_ptr(&scope),
Ordering::AcqRel, &scope);
unsafe {
scope.defer_drop(prev);
}
})
}
fn get_current(&self) -> T {
epoch::pin(|scope| {
let obj_ref = unsafe {
self.current.load(Ordering::Acquire, &scope).as_ref().unwrap()
};
obj_ref.clone()
})
}
}
当使用与问题相同的main()
运行时,它不会泄漏内存。
要考虑的一件事是,根据文档,epoch::pin()
带有SeqCst
围栏和一些原子操作的成本。 (请注意,epoch::pin()
在Crossbeam下也不是免费的,实际上要贵得多。)现代硬件上10-15 ns的延迟可能与大多数用途无关,但用户在使用时应该意识到这一点。编写试图从无锁操作中挤出每纳秒的代码。