异常安全

尽管程序应该谨慎使用展开,但有很多代码*可能*会 panic。如果你解包了一个 None,索引越界,或者除以 0,你的程序就会 panic。在调试构建中,如果发生溢出,每个算术运算都可能 panic。除非你非常小心并严格控制运行的代码,否则几乎所有东西都可能展开,你需要为此做好准备。

在更广泛的编程世界中,为展开做好准备通常被称为*异常安全*。在 Rust 中,有两个级别的异常安全需要注意

  • 在不安全的代码中,我们*必须*保证异常安全,以防止违反内存安全。我们称之为*最小*异常安全。

  • 在安全的代码中,保证异常安全以使你的程序做正确的事情是*好的*。我们称之为*最大*异常安全。

与 Rust 中的许多地方一样,当涉及到展开时,不安全的代码必须准备好处理糟糕的安全的代码。暂时创建不健全状态的代码必须小心,不要因为 panic 而导致该状态被使用。通常这意味着要确保在这些状态存在时只运行非 panic 代码,或者在发生 panic 时创建一个清理状态的守卫。这并不一定意味着 panic 遇到的状态是一个完全一致的状态。我们只需要保证它是一个*安全*的状态。

大多数不安全的代码都是叶状的,因此很容易实现异常安全。它控制着所有运行的代码,而这些代码中的大多数都不能 panic。然而,不安全的代码在重复调用调用者提供的代码时,使用临时未初始化数据的数组并不少见。这样的代码需要小心并考虑异常安全。

Vec::push_all

Vec::push_all 是一个临时技巧,可以在没有特化的情况下可靠地高效地扩展 Vec 切片。这是一个简单的实现

impl<T: Clone> Vec<T> {
    fn push_all(&mut self, to_push: &[T]) {
        self.reserve(to_push.len());
        unsafe {
            // can't overflow because we just reserved this
            self.set_len(self.len() + to_push.len());

            for (i, x) in to_push.iter().enumerate() {
                self.ptr().add(i).write(x.clone());
            }
        }
    }
}

我们绕过了 push,以避免对我们明确知道有容量的 Vec 进行冗余的容量和 len 检查。逻辑完全正确,但我们的代码有一个微妙的问题:它不是异常安全的!set_lenaddwrite 都很好;clone 是我们忽略的炸弹。

Clone 完全不受我们控制,并且完全可以自由地 panic。如果它这样做了,我们的函数将提前退出,并且 Vec 的长度设置过大。如果查看或丢弃 Vec,将读取未初始化的内存!

在这种情况下,修复方法相当简单。如果我们想保证我们*确实*克隆的值被丢弃,我们可以在每次循环迭代时设置 len。如果我们只想保证不能观察到未初始化的内存,我们可以在循环之后设置 len

BinaryHeap::sift_up

将元素向上冒泡到堆中比扩展 Vec 要复杂一些。伪代码如下

bubble_up(heap, index):
    while index != 0 && heap[index] < heap[parent(index)]:
        heap.swap(index, parent(index))
        index = parent(index)

将这段代码逐字翻译成 Rust 是完全可以的,但有一个烦人的性能特征:self 元素被无用地反复交换。我们宁愿有以下内容

bubble_up(heap, index):
    let elem = heap[index]
    while index != 0 && elem < heap[parent(index)]:
        heap[index] = heap[parent(index)]
        index = parent(index)
    heap[index] = elem

这段代码确保每个元素被复制的次数尽可能少(实际上,elem 通常需要被复制两次)。然而,它现在暴露了一些异常安全问题!在任何时候,都存在一个值的两个副本。如果我们在这个函数中 panic,某些东西会被双重丢弃。不幸的是,我们也无法完全控制代码:比较是由用户定义的!

与 Vec 不同,这里的修复方法并不容易。一种选择是将用户定义的代码和不安全的代码分成两个独立的阶段

bubble_up(heap, index):
    let end_index = index;
    while end_index != 0 && heap[end_index] < heap[parent(end_index)]:
        end_index = parent(end_index)

    let elem = heap[index]
    while index != end_index:
        heap[index] = heap[parent(index)]
        index = parent(index)
    heap[index] = elem

如果用户定义的代码崩溃了,那就没问题了,因为我们还没有真正触碰堆的状态。一旦我们开始摆弄堆,我们就只使用我们信任的数据和函数,所以不用担心 panic。

也许你对这个设计不满意。这肯定是作弊!而且我们必须*两次*进行复杂的堆遍历!好吧,让我们硬着头皮上吧。让我们*真正地*混合不可信和不安全的代码。

如果 Rust 像 Java 一样有 tryfinally,我们可以这样做

bubble_up(heap, index):
    let elem = heap[index]
    try:
        while index != 0 && elem < heap[parent(index)]:
            heap[index] = heap[parent(index)]
            index = parent(index)
    finally:
        heap[index] = elem

基本思想很简单:如果比较 panic,我们只需将松散的元素扔到逻辑上未初始化的索引中,然后退出。任何观察堆的人都会看到一个可能*不一致*的堆,但至少它不会导致任何双重丢弃!如果算法正常终止,那么这个操作恰好与我们无论如何完成的方式一致。

遗憾的是,Rust 没有这样的结构,所以我们需要自己动手!方法是将算法的状态存储在一个单独的结构体中,并使用析构函数来处理“finally”逻辑。无论我们是否 panic,析构函数都会运行并为我们清理。

struct Hole<'a, T: 'a> {
    data: &'a mut [T],
    /// `elt` is always `Some` from new until drop.
    elt: Option<T>,
    pos: usize,
}

impl<'a, T> Hole<'a, T> {
    fn new(data: &'a mut [T], pos: usize) -> Self {
        unsafe {
            let elt = ptr::read(&data[pos]);
            Hole {
                data,
                elt: Some(elt),
                pos,
            }
        }
    }

    fn pos(&self) -> usize { self.pos }

    fn removed(&self) -> &T { self.elt.as_ref().unwrap() }

    fn get(&self, index: usize) -> &T { &self.data[index] }

    unsafe fn move_to(&mut self, index: usize) {
        let index_ptr: *const _ = &self.data[index];
        let hole_ptr = &mut self.data[self.pos];
        ptr::copy_nonoverlapping(index_ptr, hole_ptr, 1);
        self.pos = index;
    }
}

impl<'a, T> Drop for Hole<'a, T> {
    fn drop(&mut self) {
        // fill the hole again
        unsafe {
            let pos = self.pos;
            ptr::write(&mut self.data[pos], self.elt.take().unwrap());
        }
    }
}

impl<T: Ord> BinaryHeap<T> {
    fn sift_up(&mut self, pos: usize) {
        unsafe {
            // Take out the value at `pos` and create a hole.
            let mut hole = Hole::new(&mut self.data, pos);

            while hole.pos() != 0 {
                let parent = parent(hole.pos());
                if hole.removed() <= hole.get(parent) { break }
                hole.move_to(parent);
            }
            // Hole will be unconditionally filled here; panic or not!
        }
    }
}