Rust 使用 RAII (Resource Acquisition Is Initialization) 来管理资源:对象初始化会导致资源的初始化,而对象释放时会导致资源的释放。

Mutex 为例:

{
    let guard = m.lock();
    // do something
}
// guard freed out of scope.
{
    // we can acquire this lock again.
    let guard = m.lock();
}

guard 离开当前 scope 的时,rust 会保证 guarddrop 被自动调用:

#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Drop for MutexGuard<'_, T> {
    #[inline]
    fn drop(&mut self) {
        unsafe {
            self.lock.poison.done(&self.poison);
            self.lock.inner.raw_unlock();
        }
    }
}
  • 如果对应的类型有自己的 Drop 实现,rust 会调用 Drop::drop()
  • 否则则递归对每个字段执行自动生成的 drop 实现

Drop 的 trait 定义如下:

pub trait Drop {
    fn drop(&mut self);
}

非常简单,但是在实际的使用过程中还是很容易踩坑。今天的这期周报就结合一些实际的 BUG 来聊聊我的踩坑经历。

__var 的行为差异

let _var = abc; 的语义是十分明确的:创建一个新的绑定,他的生命周期会持续到当前 scope 结束:

struct Test(&'static str);

impl Drop for Test {
    fn drop(&mut self) {
        println!("Test with {} dropped", self.0)
    }
}

fn main() {
    {
        println!("into scope");
        let _abc = Test("_abc");
        println!("leave scope");
    }
}

其执行结果如下:

into scope
leave scope
Test with _abc dropped

但是 let _ = abc; 的语义却更晦涩一些:不要将后面的表达式绑定为任何东西。它只是一个 match 表达式,本身并不会导致 drop,之所以我们观察到 drop 是因为它 match 的值本身就是临时的。

很多人将其理解为等价于 drop(abc) 或者 abc; 是错误的,这里有一个反例

struct Test(&'static str);

impl Drop for Test {
    fn drop(&mut self) {
        println!("Test with {} dropped", self.0)
    }
}

fn main() {
    let x = Test("x");
    {
        println!("into scope");
        let _ = x;
        println!("leave scope");
    }
}

其执行结果如下:

into scope
leave scope
Test with x dropped

将其理解为 no-op,也是片面的。我们同样能找出一个反例

struct Test(&'static str);

impl Drop for Test {
    fn drop(&mut self) {
        println!("Test with {} dropped", self.0)
    }
}

fn main() {
    println!("into scope");
    let _ = Test("x");
    println!("leave scope");
}

其执行结果为:

into scope
Test with x dropped
leave scope

这里还有一些更有趣的例子

struct Test(&'static str);

impl Drop for Test {
    fn drop(&mut self) {
        println!("Test with {} dropped", self.0)
    }
}

fn main() {
    match (Test("a"), Test("b"), Test("c")) {
        (a, _, c) => {
            println!("match arm");
        }
    }
    println!("after match");
}

其执行结果为:

match arm
Test with c dropped
Test with a dropped
Test with b dropped
after match

Test("b") 的生命周期一直延续到这个 match 语句的最后。

综上所述,let _ = abc; 是 match pattern 的延续,其实际的行为是受具体的表达式制约的。我们不应当依赖 let _ = abc; 实现任何 drop 的逻辑,其唯一合理用途是用来标记变量不再使用,以免除 #[must_use] 警告:

// remove file, but don't care about its result.
let _ = fs::remove_file("a.txt");

在实际的业务逻辑中,我们时常会忽略这一点,以 Databend 最近修复的一个 BUG 为例:Bug: runtime spawn_batch does not release permit correctly。Databend 为了控制 IO 的并发数量,使用 semaphore 来控制任务的并行度。本来期望的时候在任务执行完毕后再释放,但是代码中使用了 let _ = permit,导致 permit 释放时机不符合预期,进而导致任务的并发控制不符合预期:

 let handler = self.handle.spawn(async move {
     // take the ownership of the permit, (implicitly) drop it when task is done
-    let _ = permit;
+    let _pin = permit;
     fut.await
 });

如何手动调用 drop

处于显而易见的原因,Drop::drop() 不允许被手动调用,否则非常容易出现 double free 的问题,Rust 在编译器就会对这样的调用报错。如果想要控制变量的 drop,可以使用 std::mem::drop 函数,它的原理非常简单:Move 这个变量,然后不返回任何东西。

#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
#[cfg_attr(not(test), rustc_diagnostic_item = "mem_drop")]
pub fn drop<T>(_x: T) {}

本质上相当于:

let x = Test {};

{
    x;
}

但是需要注意,对实现了 Copy 的类型来说,调用 drop 是没有意义的:

  • 编译器会自行维护 Copy 类型在栈上的数据,不能为 Copy 类型实现 Drop trait
  • 对 Copy 类型调用 drop 总是会复制当前变量然后释放

致谢

感谢 @drmingdrmer & @zhang2014 的讨论,纠正了我的错误观点

参考资料