DEV Community

Cover image for Rust Notes on Temporary values (usage of Mutex) - 3
Nirmalya Sengupta
Nirmalya Sengupta

Posted on

Rust Notes on Temporary values (usage of Mutex) - 3

[Cover image credit: https://pixabay.com/users/engin_akyurt-3656355]

Build up

Perhaps, it will be easier to understand the background of this article, if the ones just preceding it (in the series) are referred to:

  1. Rust Notes on Temporary values (usage of Mutex) - 1 (link)

    Explores Method-call expressions and binding.

  2. Rust Notes on Temporary values (usage of Mutex) - 2 (link)

    Explores RAII, OBMR and how these are used in establishing the behaviour of a Mutex

Mutex is used to fence a piece of data. Any access to this data, is preceded by acquiring a lock on it. Releasing this lock is crucial, because unless that happens, subsequent access to this data will be impossible. In the preceding article, we have briefly seen how a combination of RAII (or OBMR) and Scoping rules of Rust, makes this very straightforward. However, are there cases where the lock is held for longer than we expect?

Revisiting accessing the fenced data

We have seen this snippet earlier.

use std::sync::Mutex;
fn main() {
    let a_mutex = Mutex::new(5);
    let stringified = a_mutex.lock().unwrap().to_string(); // multiple transformations here!    
    println!("stringified -> {:?}", stringified);
    println!("Mutex itself -> {:?}", a_mutex);
    println!("Good bye!");
}
Enter fullscreen mode Exit fullscreen mode

a_mutex.lock().unwrap().to_string(): In that expression, multiple steps are taking place:

  • Lock is obtained from a_mutex. An unnamed MutexGuard object is produced.
  • The MutexGuard is unwrapped and its internal i32 ( value of '5') is made available (through a mechanism of deref but that is not important here)
  • The to_string() method available on i32 is called.
  • A stringified representation of numeric '5' is produced. This is the final value of the expression.
  • Then the expression is complete and the statement ends. Unnamed MutexGuard goes out of scope and hence, dropped. The lock is released.

This stringified representation is assigned to (bound to) stringified.

Importantly, just before that ;, the scope of unnamed MutexGuard ends, and it is dropped, thereby releasing the lock. That is as well, because once we call to_string(), our objective has been fulfilled.

Can we force the guard to stay open for longer?

The MutexGuard is dropped when the statement ends. What if we want the guard to exist, till we want?

We have already seen this:

use std::sync::Mutex;

fn main() {
    let a_mutex = Mutex::new(5);
    let guard = a_mutex.lock().unwrap(); // <-- 'guard' is of type `MutexGuard`
    println!("guard -> {:?}", guard);
    println!("Mutex itself -> {:?}", a_mutex);

    // An attempt to acquire the lock again.
    let w = a_mutex.try_lock().unwrap();
    println!("Good bye!");
}
Enter fullscreen mode Exit fullscreen mode

The output clearly shows that the mutex is in a locked state and an attempt to acquire the lock again is disallowed:

guard -> 5
Mutex itself -> Mutex { data: <locked>, poisoned: false, .. }
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "WouldBlock"', src/main.rs:37:32
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Enter fullscreen mode Exit fullscreen mode

Why? Because the MutexGuard has not gone out of scope. It is now bound to a variable guard; therefore it is not temporary anymore. In fact the MutexGuard's scope now spans till the end of the block, i.e. end of main in this case.

Clearly, the scope of MutexGuard ends when we operate directly on it by calling a method on value it is guarding but not when we bind it to a variable, and then operate on it. Does any other case exist, where the scope is extended?

Making use of match expression

lock() returns a LockResult; therefore, we can match on it:

use std::sync::Mutex;
fn main() {


    let a_mutex = Mutex::new(5);
    let stringified = match a_mutex.lock() { // <--- scope begins

        Ok(guard) => {
            let v = guard.to_string();
            println!("Mutex itself on a match arm -> {:?}", a_mutex);
            v
        }, 

        Err(e) => panic!("We don't expect it here {}!", e.get_ref()) // 'e' carries the guard inside it
    }; // <-- scope ends

    println!("stringified -> {:?}", stringified);
    println!("Mutex itself after guard is released -> {:?}", a_mutex);
    println!("Good bye!");
}
Enter fullscreen mode Exit fullscreen mode

The output confirms that the lock is released, but after the entire match expression, ends:

Mutex itself while guard is alive, on a match arm -> Mutex { data: <locked>, poisoned: false, .. }
stringified -> "5"
Mutex itself after guard is released -> Mutex { data: 5, poisoned: false, .. }
Good bye!
Enter fullscreen mode Exit fullscreen mode

So, the temporary here is being held inside the arm. Let us go a bit further.

In this case:

 use std::sync::Mutex;
fn main() {
    let a_mutex = Mutex::new(5);

    let stringified = a_mutex.lock().unwrap().to_string(); // <-- MutexGuard's scope ends heres
    println!("stringified -> {:?}", stringified);
    println!("Mutex itself after guard is released -> {:?}", a_mutex);
    println!("Good bye!");
}
Enter fullscreen mode Exit fullscreen mode

The lock is released at the end of the statement ( conceptually, when ; is reached ).

But, not in this case:

 use std::sync::Mutex;
fn main() {

    let a_mutex = Mutex::new(5);
    let stringified = match a_mutex.lock().unwrap().to_string().as_str() {

        s @ "5" => {
            println!("We have received a five!");
            println!("Mutex itself while guard is alive, on a match arm -> {:?}", a_mutex);
            s.to_owned()
        },
        _ => "Not a five".to_owned()
    };  // <-- lock is released here!

    println!("stringified -> {:?}", stringified);
    println!("Mutex itself after guard is released -> {:?}", a_mutex);
}
Enter fullscreen mode Exit fullscreen mode

Note: That .as_str() is required for idiomatically *match*ing String literals. It has got nothing to do with the core discussion here. The basic point remains the same: how long is the lock being acquired and held?


The scoping rules come into play. The expression that follows a match is called a Scrutinee Expression. In case a scrutinee expression generates a temporary value (viz., MutexGuard) it is not dropped till the end of scope of match expression. As a consequence, the lock is held, till the match expression comes to an end.

This has implications which may not be obvious. For example, what happens in this case?

use std::sync::Mutex;
fn main() {

    let a_mutex = Mutex::new(5);
    let stringified = match a_mutex.lock().unwrap().to_string().as_str() {

        s @ "5" => {
            println!("Mutex itself while guard is alive, on a match arm -> {:?}", a_mutex);
            // the lock is held here..
            let p = do_some_long_calculation(); // Example function, not implemented
            // the lock is still held here..
            let q = make_a_network_call(&p);    // Example function, not implemented
            s.to_owned() + q
        },
        _ => "Not a five".to_owned()
    };  // <-- lock is released here!

    println!("stringified -> {:?}", stringified);
    println!("Mutex itself after guard is released -> {:?}", a_mutex);
}
Enter fullscreen mode Exit fullscreen mode

Executing functions, which take time to finish, will effectively prevent the lock from being released. If someone else is waiting for the lock (may be, another thread), tough luck!

Before we close, let's contrast the code above, with the code below:

use std::sync::Mutex;
fn main() {
    let a_mutex = Mutex::new(5);

    let unfenced = a_mutex.lock().unwrap().to_string(); // <-- MutexGuard goes out of scope

    let stringified = match unfenced.as_str() { // like earlier, ignore the as_str() function

        s @ "5" => {
            println!("We have received a five!");
            println!("Mutex itself while guard is alive, on a match arm -> {:?}", a_mutex);
            s.to_owned()
        },
        _ => "Not a five".to_owned()
    };

    println!("stringified -> {:?}", stringified);
    println!("Mutex itself after guard is released -> {:?}", a_mutex);
    println!("Good bye!");
}
Enter fullscreen mode Exit fullscreen mode

Main Takeaways

  • In order to extend a temporary value's scope, it is bound to a variable using the let statement.
  • In case of MutexGuard, a let statement forces the guard to exist for longer (depends on the scope of the let variable). While the guard exists, the lock on the innter piece of data remains acquired.
  • In case using match expression that works on a temporary, the scope extends till the end of the match expression.
  • Because an arm of a match can execute arbitrary logic, the lock can be held for longer than expected or even, necessary. This is an important observation.

Acknowledgements

In my quest to understand the behaviour of the compiler in this case, I have gone through a number of articles / blogs / explanations. I will be unfair and utterly discourteous on my part, if I don't mention the some of them. This blog stands on the shoulders of those authors and elaborators:

  • My specific question on stackoverflow and fantastic answers given by Chayim Friedman, masklinn, and paul.
  • This stackoverflow QnA On, how the lock should be treated, in order to make use of the value tucked inside the mutex.
  • This post has been extremely valuable for me to understand the issue of interplay between Place Context and Value Context! Thank you Lukas Kalberbolt. A great question is posed by AnimatedRNG.

Top comments (0)