泄漏

基于所有权的资源管理旨在简化组合。您在创建对象时获取资源,并在销毁对象时释放资源。由于销毁由您处理,这意味着您不会忘记释放资源,并且会尽快发生!这当然很完美,我们所有的问题都解决了。

一切都很糟糕,我们有新的和奇特的问题需要尝试解决。

许多人喜欢相信 Rust 消除了资源泄漏。在实践中,这基本上是正确的。您会惊讶地发现安全的 Rust 程序以不受控制的方式泄漏资源。

然而,从理论的角度来看,无论您如何看待它,这绝对不是事实。“泄漏”的概念非常抽象,以至于无法预防。在程序开始时初始化一个集合,用大量带有析构函数的对象填充它,然后进入一个永远不会引用它的无限事件循环,这是非常简单的。该集合将毫无用处地闲置,持有其宝贵的资源,直到程序终止(此时所有这些资源无论如何都会被操作系统回收)。

我们可以考虑一种更受限制的泄漏形式:未能删除无法访问的值。Rust 也不能阻止这种情况。事实上,Rust *有一个函数可以做到这一点*:`mem::forget`。此函数消耗传递给它的值,*然后不运行其析构函数*。

过去,`mem::forget` 被标记为不安全,作为一种防止使用它的提示,因为不调用析构函数通常不是一种良好的行为(尽管对某些特殊的非安全代码很有用)。然而,这通常被认为是一种站不住脚的立场:在安全代码中,有很多方法可以不调用析构函数。最著名的例子是使用内部可变性创建引用计数指针的循环。

安全代码可以合理地假设不会发生析构函数泄漏,因为任何泄漏析构函数的程序都可能是错误的。然而,*非安全*代码不能依赖于析构函数的运行来保证安全。对于大多数类型来说,这无关紧要:如果您泄漏了析构函数,那么该类型根据定义是不可访问的,所以这无关紧要,对吧?例如,如果您泄漏了一个 `Box<u8>`,那么您会浪费一些内存,但这几乎不会违反内存安全。

然而,我们必须小心析构函数泄漏的地方是*代理*类型。这些类型管理对不同对象的访问,但实际上并不拥有它。代理对象非常罕见。您需要关心的代理对象就更少了。但是,我们将重点介绍标准库中的三个有趣的例子

  • vec::Drain
  • Rc
  • thread::scoped::JoinGuard

Drain

`drain` 是一个集合 API,它将数据移出容器而不消耗容器。这使我们能够在声明对 `Vec` 的所有内容的所有权后重用其分配。它生成一个迭代器 (Drain),该迭代器按值返回 Vec 的内容。

现在,考虑迭代过程中的 Drain:一些值已被移出,而另一些则没有。这意味着 Vec 的一部分现在充满了逻辑上未初始化的数据!我们可以在每次删除一个值时向后移动 Vec 中的所有元素,但这会对性能造成灾难性的后果。

相反,我们希望 Drain 在其被删除时修复 Vec 的后备存储。它应该运行到完成,向后移动任何未被删除的元素(drain 支持子范围),然后修复 Vec 的 `len`。它甚至是展开安全的!很简单!

现在考虑以下内容

let mut vec = vec![Box::new(0); 4];

{
    // start draining, vec can no longer be accessed
    let mut drainer = vec.drain(..);

    // pull out two elements and immediately drop them
    drainer.next();
    drainer.next();

    // get rid of drainer, but don't call its destructor
    mem::forget(drainer);
}

// Oops, vec[0] was dropped, we're reading a pointer into free'd memory!
println!("{}", vec[0]);

这显然是不好的。不幸的是,我们有点进退两难:在每一步都保持一致的状态会付出巨大的代价(并且会抵消 API 的任何好处)。未能保持一致的状态会导致安全代码中的未定义行为(使 API 不健全)。

那么我们能做什么呢?好吧,我们可以选择一个微不足道的一致状态:在开始迭代时将 Vec 的 len 设置为 0,并在析构函数中根据需要修复它。这样,如果一切正常执行,我们将在开销最小的情况下获得所需的行为。但是,如果有人*胆敢*在迭代过程中对我们使用 mem::forget,那么所做的只是*泄漏更多*(并且可能使 Vec 处于意外但一致的状态)。由于我们已经接受 mem::forget 是安全的,所以这绝对是安全的。我们将导致更多泄漏的泄漏称为*泄漏放大*。

Rc

Rc 是一个有趣的例子,因为乍一看它似乎根本不是一个代理值。毕竟,它管理着它指向的数据,并且删除一个值的 所有 Rc 也会删除该值。泄漏一个 Rc 似乎并不是特别危险。它会使引用计数永久递增,并阻止数据被释放或删除,但这看起来就像 Box 一样,对吧?

不。

让我们考虑一个简化的 Rc 实现

struct Rc<T> {
    ptr: *mut RcBox<T>,
}

struct RcBox<T> {
    data: T,
    ref_count: usize,
}

impl<T> Rc<T> {
    fn new(data: T) -> Self {
        unsafe {
            // Wouldn't it be nice if heap::allocate worked like this?
            let ptr = heap::allocate::<RcBox<T>>();
            ptr::write(ptr, RcBox {
                data,
                ref_count: 1,
            });
            Rc { ptr }
        }
    }

    fn clone(&self) -> Self {
        unsafe {
            (*self.ptr).ref_count += 1;
        }
        Rc { ptr: self.ptr }
    }
}

impl<T> Drop for Rc<T> {
    fn drop(&mut self) {
        unsafe {
            (*self.ptr).ref_count -= 1;
            if (*self.ptr).ref_count == 0 {
                // drop the data and then free it
                ptr::read(self.ptr);
                heap::deallocate(self.ptr);
            }
        }
    }
}

这段代码包含一个隐含的和微妙的假设:`ref_count` 可以放入一个 `usize` 中,因为内存中不能有超过 `usize::MAX` 个 Rc。然而,这本身就假设 `ref_count` 准确地反映了内存中 Rc 的数量,而我们知道这在使用 `mem::forget` 时是错误的。使用 `mem::forget`,我们可以使 `ref_count` 溢出,然后在有未完成的 Rc 的情况下将其降至 0。然后,我们可以愉快地对内部数据进行释放后使用。非常糟糕,不好。

这可以通过检查 `ref_count` 并做*一些事情*来解决。标准库的立场是直接中止,因为您的程序已经变得非常糟糕。而且*我的天哪*,这是一个多么荒谬的极端情况。

thread::scoped::JoinGuard

注意:此 API 已从 std 中删除,有关更多信息,您可以参考问题 #24292

本节保留在此处,因为我们认为此示例仍然很重要,无论它是否是 std 的一部分。

thread::scoped API 旨在允许生成引用其父线程堆栈数据的线程,而无需对该数据进行任何同步,方法是确保父线程在任何共享数据超出范围之前加入该线程。

pub fn scoped<'a, F>(f: F) -> JoinGuard<'a>
    where F: FnOnce() + Send + 'a

这里 `f` 是另一个线程要执行的一些闭包。说 `F: Send + 'a` 是说它关闭了生存期为 `'a` 的数据,并且它要么拥有该数据,要么该数据是 Sync(意味着 `&data` 是 Send)。

因为 JoinGuard 有一个生命周期,所以它会将其关闭的所有数据借用在父线程中。这意味着 JoinGuard 的生存期不能超过另一个线程正在处理的数据。当 JoinGuard *确实*被删除时,它会阻塞父线程,确保子线程在任何关闭的数据在父线程中超出范围之前终止。

用法如下所示

let mut data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
{
    let mut guards = vec![];
    for x in &mut data {
        // Move the mutable reference into the closure, and execute
        // it on a different thread. The closure has a lifetime bound
        // by the lifetime of the mutable reference `x` we store in it.
        // The guard that is returned is in turn assigned the lifetime
        // of the closure, so it also mutably borrows `data` as `x` did.
        // This means we cannot access `data` until the guard goes away.
        let guard = thread::scoped(move || {
            *x *= 2;
        });
        // store the thread's guard for later
        guards.push(guard);
    }
    // All guards are dropped here, forcing the threads to join
    // (this thread blocks here until the others terminate).
    // Once the threads join, the borrow expires and the data becomes
    // accessible again in this thread.
}
// data is definitely mutated here.

原则上,这完全可行!Rust 的所有权系统完美地确保了这一点!...除非它依赖于被调用的析构函数来保证安全。

let mut data = Box::new(0);
{
    let guard = thread::scoped(|| {
        // This is at best a data race. At worst, it's also a use-after-free.
        *data += 1;
    });
    // Because the guard is forgotten, expiring the loan without blocking this
    // thread.
    mem::forget(guard);
}
// So the Box is dropped here while the scoped thread may or may not be trying
// to access it.

糟糕。析构函数的运行对 API 至关重要,但为了采用完全不同的设计,它不得不被放弃。