3

2022-41: Rust Drop 踩坑分享

 1 year ago
source link: https://xuanwo.io/reports/2022-41/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

2022-41: Rust Drop 踩坑分享

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 的讨论,纠正了我的错误观点


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK