非词法生命周期借用检查器是否会过早释放锁?

Will the non-lexical lifetime borrow checker release locks prematurely?

我读过 What are non-lexical lifetimes?。使用非词法借用检查器,编译以下代码:

fn main() {
    let mut scores = vec![1, 2, 3];
    let score = &scores[0]; // borrows `scores`, but never used
                            // its lifetime can end here

    scores.push(4);         // borrows `scores` mutably, and succeeds
}

在上面的例子中看起来很合理,但是当涉及到互斥锁时,我们不希望它被过早地释放。

在下面的代码中,我想先锁定一个共享结构,然后执行一个闭包,主要是为了避免死锁。但是,我不确定是否会提前释放锁。

use lazy_static::lazy_static; // 1.3.0
use std::sync::Mutex;

struct Something;

lazy_static! {
    static ref SHARED: Mutex<Something> = Mutex::new(Something);
}

pub fn lock_and_execute(f: Box<Fn()>) {
    let _locked = SHARED.lock(); // `_locked` is never used.
                                 // does its lifetime end here?
    f();
}

Rust 是否对锁进行了特殊处理,以确保它们的生命周期可以延长到作用域的末尾?我们是否必须显式使用该变量以避免过早释放锁,如以下代码所示?

pub fn lock_and_execute(f: Box<Fn()>) {
    let locked = SHARED.lock(); // - lifetime begins
    f();                        // |
    drop(locked);               // - lifetime ends
}

Does Rust treat locks specially, so that their lifetimes are guaranteed to extend to the end of their scope?

没有。这是 every 类型的默认值,与借用检查器无关。

Must we use that variable explicitly to avoid premature dropping of the lock

没有

您需要做的就是确保锁守卫绑定到一个变量。您的示例执行此操作 (let _lock = ...),因此锁将在作用域末尾被删除。如果您改用 _ 模式,锁会立即被删除:

您可以通过测试锁是否确实被删除来证明这一点:

pub fn lock_and_execute() {
    let shared = Mutex::new(Something);

    println!("A");
    let _locked = shared.lock().unwrap();

    // If `_locked` was dropped, then we can re-lock it:
    println!("B");
    shared.lock().unwrap();

    println!("C");
}

fn main() {
    lock_and_execute();
}

这段代码会死锁,因为同一个线程两次尝试获取锁。

您也可以尝试使用需要 &mut self 的方法来查看不可变借用仍由守卫持有,尚未删除:

pub fn lock_and_execute() {
    let mut shared = Mutex::new(Something);

    println!("A");
    let _locked = shared.lock().unwrap();

    // If `_locked` was dropped, then we can re-lock it:
    println!("B");
    shared.get_mut().unwrap();

    println!("C");
}
error[E0502]: cannot borrow `shared` as mutable because it is also borrowed as immutable
  --> src/main.rs:13:5
   |
9  |     let _locked = shared.lock().unwrap();
   |                   ------ immutable borrow occurs here
...
13 |     shared.get_mut().unwrap();
   |     ^^^^^^^^^^^^^^^^ mutable borrow occurs here
...
16 | }
   | - immutable borrow might be used here, when `_locked` is dropped and runs the `Drop` code for type `std::sync::MutexGuard`

另请参阅:

  • Why does _ destroy at the end of statement?

这里有个误区:NLL(非词法生命周期)影响的是borrow-checks,而不是真正的lifetime对象。

Rust 广泛使用 RAII1,因此 Drop 许多对象(例如锁)的实现具有副作用 发生在执行流程中一个确定且可预测的点。

NLL 没有改变此类对象的生命周期,因此它们的析构函数在与之前完全相同的位置执行:在它们的词法范围的末尾,以相反的创建顺序执行。

NLL 确实改变了编译器对使用生命周期进行借用检查的理解。实际上,这不会导致任何代码更改;这纯粹是分析。此分析变得更聪明,以更好地识别使用引用的实际范围:

  • 在 NLL 之前,引用被认为是 "in use" 从创建到删除的那一刻,通常是它的词法范围(因此得名)。
  • NLL,而是:
    • 如果可能,尝试推迟 "in use" 跨度的开始。
    • 以引用的最后一次使用结束 "in use" 跨度。

Ref<'a> 的情况下(来自 RefCell),Ref<'a> 将在词法范围的末尾被删除,此时它将 使用RefCell的引用来减少计数器。

NLL 不会剥离抽象层,因此必须考虑任何包含引用的对象(例如Ref<'a>可能 在其 Drop 实现中访问所述引用。因此,任何包含引用的对象(例如锁)都将强制 NLL 认为引用的 "in use" 范围会扩展,直到它们被删除。

1 Resource Acquisition Is Initialization,其本义是变量构造函数一旦执行完毕,就获得了需要的资源,不在半生不熟的状态,通常用来表示销毁该变量将释放它拥有的所有资源。