2017-09-17 54 views
6

我在crossbeam中遇到了回收内存的问题。假设你正在实现一个简单的线程安全的无锁容器,它只保存一个值。任何线程都可以获得存储值的克隆,并且可以随时更新值,然后读者开始观察新值的克隆。带横梁的内存回收::时代

虽然典型的用例是指定像Arc<X>为T,执行不能依靠T为指针大小 - 例如,X可能是一个特点,导致脂肪指针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_currentget_current可以被称为数百万次没有泄漏。 (过程监视器显示由于假想gc而导致的小内存振荡,如预期的那样,但没有长期增长。)但是,当T是分配的类型时,例如,人们可以很容易地观察到泄漏。例如:

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的时代回收“不运行析构函数,而只是释放内存”。 Treiber堆栈示例中的try_pop使用ptr::read(&(*head).data)head.data中包含的值移出目标为释放的head对象。数据对象的所有权被转移给调用者,调用者可以将它移动到其他地方,或者当它超出范围时将其解除分配。

这将如何转化为上面的代码?设置者是guard.unlinked的合适位置,还是如何确保drop在底层对象上运行?取消明确drop(ptr::read(*prev))的注释会导致失败的断言,检查单调性,可能表明过早的重新分配。

回答

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公司的基于划时代的垃圾收集器,目前正在重新设计和重新编写,以便:

  • 允许延期下降,任意递延功能
  • 增量收集垃圾,以尽量减少停顿
  • 避免过度拥挤线程本地垃圾袋
  • 更热切地收集大块垃圾
  • 在API中确定一个健全的问题

如果您想了解有关Crossbeam新设计的更多信息,请查看RFCs存储库。我建议从RFC on new AtomicRFC on new GC开始。

我创建了一个实验箱,Coco,它与Crossbeam的新设计有许多共同之处。如果你现在需要一个解决方案,我建议切换到它。但请记住,只要我们发布新版本(可能是本月或下个月),Coco就会被弃用,转而支持Crossbeam。

+0

嗨Stjepan!我很高兴在这里见到你,特别是在这个非常具体的问题上,你可以看到“通常的嫌疑人”无法回答。如果你想聊天或需要任何东西,请不要犹豫在Rust聊天室中弹出:https://chat.stackoverflow.com/rooms/62927/rust –

+0

谢谢,coco正是我所需要的 - 我现在添加了来自问题代码的答案已更新为可可。对于我来说,可可最终会被弃用并不重要,那么我会转回到Crossbeam。 – user4815162342

+0

嗨,Matthieu,谢谢! :)我潜伏在房间里。同样,如果还有其他事情可以帮忙的话,请随时联系我。 –

2

作为Stjepan answered的一些细节,它是当前Crossbeam的一个已知限制,它仅支持取消分配而不是完全丢弃已经变得不可达的对象,但对其他线程仍可能是可见的。这不会影响Crossbeam支持的无锁集合,它可以自动删除集合用户“观察”的项目 - 不允许窥视。这适合队列或堆栈的需要,但不适用于例如一张无锁的地图。

这是由coco箱子解决的,它定义了几个并发集合,并作为下一代Crossbeam设计的预览。它支持延迟删除值。下面是使用COCO的Container的演绎:

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的延迟可能与大多数用途无关,但用户在编写代码时应该注意它从无锁操作中挤出每纳秒。