简体   繁体   中英

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

I encountered a deadlock while developing with Rust and WebAssembly.

Due to the use of some globally accessed variables, I choose lazy_static and a Mutex (using thread_local callbacks would have caused nesting problems). I have declared a lot of Rust functions are used by JavaScript through #[wasm_bindgen] . They read and write the lazy_static variables.

After one of the functions panics, the mutex lock cannot be released, causing other functions to panic if they need to use the same mutex.

I know that the panic problem is unexpected and needs to be fixed, but these functions are relatively independent of each other. Although the reading and writing of the lazy_static variables intersect, a certain bug may not necessarily affect other parts.

How do I trigger the release of the Mutex after a panic in Wasm to allow other calls to be ok? Is there any better practice for this kind of problem?

Rust:

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();
});

After the panic, I can not call the function at all and it is as if the Wasm is dead.

Before answering this question I should probably mention, that panic handling shouldn't be used as general error mechanism. They should be used for unrecoverable errors.

Citing 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.

Panics in Rust are actually much more gentle than it may seem in the first place for people coming from C++ background (which I assume is the case for some people writing in the comments). Uncaught Rust panics by default terminate thread, while C++ exception terminate whole process.

Citing 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.

It is fine tocatch_unwind and recover thread from panic, but you should know that catch_unwind isn't guaranteed to catch all panics.

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.

So, we understood that recovering from panic is fine. The question is what to do when lock is poisoned.

Citing 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).

There is a valid reason for poisoning, because invariants of your data may not be held. Consider panic! in the middle of some function. This is just an additional level of security, that you can bypass.

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

Of course it's always better to fix panic, than do this.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM