使用crossbeam :: epoch进行内存回收

use*_*342 6 lock-free rust

我在内存中遇到了内存回收的问题crossbeam.假设您正在实现一个包含单个值的简单线程安全无锁容器.任何线程都可以获取存储值的克隆,并且值可以在任何时候更新,之后读者开始观察新值的克隆.

虽然典型的用例是指定像Arc<X>T 这样的东西,但实现不能依赖于T指针大小 - 例如,X可能是一个特征,导致胖指针Arc<X>.但是对任意T的无锁访问似乎非常适合基于纪元的无锁代码.根据这些例子,我想出了这个:

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()
    }
}
Run Code Online (Sandbox Code Playgroud)

当与不分配的类型一起使用时,例如with 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

运行此程序会显示内存使用量稳定且显着增加,最终会消耗与迭代次数成比例的内存量.

根据介绍它的博客文章,Crossbeam的epoch填充"不会运行析构函数,而只是释放内存".的try_pop在堆叠二极管驱动器的示例使用ptr::read(&(*head).data)移动包含在值head.data出的head目的地为解除分配对象.数据对象的所有权将传输给调用者,调用者可以将其移动到其他地方,也可以在超出范围时解除分配.

如何转换为上面的代码?setter是适当的位置guard.unlinked,或者如何确保drop在底层对象上运行?取消注释drop(ptr::read(*prev))失败断言的显式结果,检查单调性,可能表明过早释放.

Stj*_*ina 6

问题的关键是(正如你已经想到的那样)guard.unlinked(prev)推迟执行以下代码:

drop(Vec::from_raw_parts(prev.as_raw(), 0, 1));
Run Code Online (Sandbox Code Playgroud)

但是你希望它推迟这个:

drop(Vec::from_raw_parts(prev.as_raw(), 1, 1));
Run Code Online (Sandbox Code Playgroud)

或者,等效地:

drop(Box::from_raw(prev.as_raw());
Run Code Online (Sandbox Code Playgroud)

换句话说,unlinked只是释放存储对象的内存,但不会丢弃对象本身.

这是目前Crossbeam的一个着名的痛点,但幸运的是它很快就会得到解决.Crossbeam的基于纪元的垃圾收集器目前正在进行重新设计和重写,以便:

  • 允许延迟删除和任意延迟函数
  • 逐步收集垃圾以尽量减少暂停
  • 避免过度拥挤线程本地垃圾袋
  • 更急切地收集大块垃圾
  • 修复API中的健全问题

如果您想了解有关新Crossbeam设计的更多信息,请查看RFC库.我建议从新Atomic上RFC和新GC上RFC开始.

我创造了一个实验箱,Coco,与Crossbeam的新设计有很多共同之处.如果您现在需要解决方案,我建议您切换到它.但是,请记住,可可将有利于大梁的,只要我们发布一个新版本(可能是这个或下月)已过时.