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 会保证 guard
的 drop
被自动调用:
#[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 的讨论,纠正了我的错误观点