Why lock doesn't release in else body

Greetings!

So, I understood if I get dead-lock inside if let scope, but why else scope still have lock guard?

fn get(id: u64, map: &RwLock<HashMap<u64, String>>) -> String {
    println!("0");
    if let Some(msg) = map.read().unwrap().get(&id).cloned() {
        println!("1");
        msg
    } else {
        println!("2");
        let msg = format!("msg: {id}");
        println!("3");
        map.write().unwrap().insert(id, msg.clone()); // DeadLock
        println!("4");
        msg
    }
}

Moreover, if I wrap whis lock in scope like that

if let Some(msg) = { map.read().unwrap().get(&id).cloned() } {

it still will be dead-lock inside else scope. What the heck?

Yeah, the scope extends "around" the if-else block. So a

if let Some(x) = expr {
} else {
}

can be mentally mapped as

 match expr {
    Some(x) => {}
    _ => {}
 }

What you probably want is something like:

let msg = map.read().unwrap().get(&id).cloned();
if let Some(msg) = msg {
    // If bock
} else {
   drop(msg);
   // Rest of stuff
}

This is unnecessary - msg is guaranteed to be None here, and the lock was already released anyway before if even started.

I already use that way =)

But the question wasn't "how to fix it", but "why does that happen".

Yeah, that make sense with match, but have no sense in if-else-if chain, because I can't use pattern matching with if.

But you're using it. if let Some(msg) = ... is pattern-matching, it's just that you're interested only in single branch.

Okey, when, why scope doesn't work?

if let Some(msg) = { map.read().unwrap().get(&id).cloned() } {

Of course, I can write like this:

if let Some(msg) = { || map.read().await.get(&id).cloned() }() {

but why destructor doesn't call at the end of scope?

Because of experience with C++. There destructor is called at the end of the scope which leads to subtle (and very hard to find) bugs when people assume it doesn't.

Error in the other direction (destructor is not called when people expect it to be called, but later) just leads to easy-to-fix error message during compilation, worst case. Which is easily fixable.

In C++ scope is the scope. IMHO, this (Destructors - The Rust Reference) is more confusing than C++ and lead to more errors.

Because it's scope is not what you think it is. You can find details in the reference.

In short, any temporary created during the evaluation of expression lives until the entire expression is evaluated. This means that temporaries are dropped once the expression's value is bound to a local variable or return value of the block. Without that rule, the following examples wouldn't work:

let x = func(string.as_ref());
          // ^^^^^^^^^^^^^^^ this temporary must live until `func` returns
if let Some(str_mut) = mutex.lock().unwrap().get_mut(idx) {
    str_mut.do_stuff();
 // ^^^^^^^ guard must live until the `if`-expression fully evaluates, 
 //         so that the guarded value is properly synchronized
}

To make your example work, you must explicitly drop the RwLock read guard before writing (or always acquire the write guard.

fn get(id: u64, map: &RwLock<HashMap<u64, String>>) -> String {
    println!("0");
    let read_guard = map.read().unwrap();
    if let Some(msg) = read_guard.get(&id).cloned() {
        println!("1");
        msg
    } else {
        drop(read_guard);
        println!("2");
        let msg = format!("msg: {id}");
        println!("3");
        map.write().unwrap().insert(id, msg.clone()); // DeadLock
        println!("4");
        msg
    }
}
1 Like

I got it, I got it.

I used to write in C/C++, so for me scope {} is the thing, where at the end of it things are drop. So it was surprise that this is not work in Rust.

Well, in my opinion, it's not working in C++. Short-lived temporaries are a common cause of memory safety violations. But in general, if your algorithm requires something to be dropped at specific places, it's better to call drop explicitly. It clearly communicates to the reader that the drop behaviour is very subtle, and protects both against incorrectly inferred scopes and potential future modifications of the algorithm that may miss the subtleties of drop order.

By the way, in my case just enough to make this:

let msg = map.read().unwrap().get(&id).cloned();
if let Some(msg) = msg {

and guard will be dropped.
Because of that it's confusing why guard is not dropped before else scope.