在 Wasm 中发生恐慌后,如何触发 Rust Mutex 的释放,以便以后的调用正常?

How do I trigger the release of a Rust Mutex after a panic in Wasm so that future calls will be ok?

我在使用 Rust 和 WebAssembly 进行开发时遇到了死锁。

由于使用了一些全局访问的变量,我选择了 lazy_static 和一个 Mutex(使用 thread_local 回调会导致嵌套问题)。我已经声明 JavaScript 到 #[wasm_bindgen] 使用了很多 Rust 函数。他们读写 lazy_static 个变量。

其中一个函数发生panic后,互斥锁无法释放,导致其他需要使用同一个mutex的函数发生panic。

我知道panic问题是意想不到的,需要修复,但是这些函数是相对独立的。 lazy_static变量的读写虽然有交叉,但某个bug不一定会影响到其他部分。

如何在 Wasm 中出现恐慌后触发 Mutex 的释放以允许其他调用正常?这种问题有没有更好的做法?

生锈:

use std::sync::Mutex;
use std::sync::PoisonError;
use wasm_bindgen::prelude::*;

pub struct CurrentStatus {
    pub index: i32,
}

impl CurrentStatus {
    fn new() -> Self {
        CurrentStatus { index: 1 }
    }
    fn get_index(&mut self) -> i32 {
        self.index += 1;
        self.index.clone()
    }

    fn add_index(&mut self) {
        self.index += 2;
    }
}

lazy_static! {
    pub static ref FOO: Mutex<CurrentStatus> = Mutex::new(CurrentStatus::new());
}

unsafe impl Send for CurrentStatus {}

#[wasm_bindgen]
pub fn add_index() {
    FOO.lock()
        .unwrap_or_else(PoisonError::into_inner)
        .add_index();
}

#[wasm_bindgen]
pub fn get_index() -> i32 {
    let mut foo = FOO.lock().unwrap_or_else(PoisonError::into_inner);
    if foo.get_index() == 6 {
        panic!();
    }
    return foo.get_index();
}

JavaScript:

const js = import("../pkg/hello_wasm.js");
js.then(js => {
  window.js = js;
  console.log(js.get_index());
  js.add_index();
  console.log(js.get_index());
  js.add_index();
  console.log(js.get_index());
  js.add_index();
  console.log(js.get_index());
  js.add_index();
  console.log(js.get_index());
  js.add_index();
});

panic过后,我根本无法调用函数,就好像Wasm死了一样。

在回答这个问题之前,我可能应该提一下,恐慌处理不应该用作一般错误机制。它们应该用于不可恢复的错误。

引用 documentation.

This allows a program to terminate immediately and provide feedback to the caller of the program. panic! should be used when a program reaches an unrecoverable state.

对于来自 C++ 背景的人来说,Rust 中的恐慌实际上比最初看起来要温和得多(我假设某些人在评论中写的就是这种情况)。未捕获的 Rust 恐慌默认终止线程,而 C++ 异常终止整个进程。

引用 documentation

Fatal logic errors in Rust cause thread panic, during which a thread will unwind the stack, running destructors and freeing owned resources. While not meant as a 'try/catch' mechanism, panics in Rust can nonetheless be caught (unless compiling with panic=abort) with catch_unwind and recovered from, or alternatively be resumed with resume_unwind. If the panic is not caught the thread will exit, but the panic may optionally be detected from a different thread with join. If the main thread panics without the panic being caught, the application will exit with a non-zero exit code.

可以 catch_unwind 并从恐慌中恢复线程,但您应该知道 catch_unwind 不能保证捕获所有恐慌。

Note that this function may not catch all panics in Rust. A panic in Rust is not always implemented via unwinding, but can be implemented by aborting the process as well. This function only catches unwinding panics, not those that abort the process.

因此,我们了解到从恐慌中恢复过来很好。问题是锁中毒了怎么办

引用 documentation

The mutexes in this module implement a strategy called "poisoning" where a mutex is considered poisoned whenever a thread panics while holding the mutex. Once a mutex is poisoned, all other threads are unable to access the data by default as it is likely tainted (some invariant is not being upheld).

中毒是有正当理由的,因为您的数据的不变量可能不成立。考虑一些函数中间的 panic! 。这只是一个额外的安全级别,您可以绕过它。

A poisoned mutex, however, does not prevent all access to the underlying data. The PoisonError type has an into_inner method which will return the guard that would have otherwise been returned on a successful lock. This allows access to the data, despite the lock being poisoned.

use std::sync::{Mutex, PoisonError};
fn main() {
    let mutex = Mutex::new(1);

    // We are prepared to face bugs if invariants are wrong
    println!("{}", mutex.lock().unwrap_or_else(PoisonError::into_inner));
}

Playground link

当然,修复恐慌总是比这样做更好。