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

nsengupta

Nirmalya Sengupta

Posted on June 16, 2023

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

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

Build up

In this article (first in the series), I have elaborated the understanding of temporary objects / variables in an expression, their scopes and behaviour.

This article is a continuation of the same topic, but it explores a much-used (and very crucial in multi-threaded context) type in Rust programming: the Mutex .

Quick definition for the context

The concept of a Mutex (Mutual Exclusion) is ages old. The idea is to ensure that only one thread of execution can access and modify a piece of data in memory, it must be locked first and unlocked, post-modification. All threads must govern themselves by checking for the lock's availabilty before going ahead with the modification; if the lock is not avaiable (meaning, some other thread is in the middle of modifying the data beind the lock), it must not gatecrash but wait, patiently.The Rust bool's chapter on Mutex is a recommended read.

Mutex in Rust

The crucial and tricky thing about a Mutex is about when to put in the lock around the inner data (rememember: a lock's existence is jutsified because it is guarding some piece of data in memory) and when to lift it. The sequecnce of acquiring and releasing the lock extremely important. Before we try to understand the way it is done in Rust, we should get familiarized with a term: RAII.

RAII

RAII stands for, rather too predictably, Resource Acquisition Is Initialization (phew!). C++ experts are quite familiar with it. I will not delve deep into this topic - other detailed descriptions like this, this and this and several others - exist for a deeper understanding.

Put very simply, RAII is a mechanism which stipulates that any access to a computing resource (viz., memory,file, DB Connection etc.) must be preceded by its construction. While this sounds kind of given, the subsequent part is what is important: the access to resource is denied after the destruction of the resource. In other words, the resource is available strictly between its creation and demise.The duration between these two stages of life, is determined by the scoping rules.

OBRM

I have come across this term while tinkering with Rust. OBRM stands for O*wnership *B*ased *R*esource *M*anagament (you don't like the acronym? I don't, either 😃 ). Again, I will not delve into this either (I find these 2 good videos by #letsgorusty here and here, quite explanatory). The main understanding is this: acquisition and release of resources are governed by the *Ownership rules of Rust. Put simply, when the object (say, a constructed struct) that holds the resource is destroyed ( *Drop*ped ), the resource is released too. Automatically! The language ensures this behavior!

My objective is to elucidate how this idiom is used in Rust's handling of Mutex.

Experiments and observations

We hold a simple i32 value inside a Mutex, then operate on it, observe the behavior of the code and convince ourselves why the behavior is that way!

use std::sync::Mutex;
fn main() {
let a_mutex = Mutex::new(5);         // Just an i32 value inside a mutex
    let guard = a_mutex.lock().unwrap(); // <-- 'guard' is of type `MutexGuard`
    println!("guard -> {:?}", guard);
    println!("Mutex itself -> {:?}", a_mutex);
    println!("Good bye!");
}
Enter fullscreen mode Exit fullscreen mode

The output is:

guard -> 5
Mutex itself -> Mutex { data: <locked>, poisoned: false, .. }
Good bye!
Enter fullscreen mode Exit fullscreen mode

Inside itself, the Mutex is holding its own data in a Locked state. Then the program ends, and the lock is released. But, where is that MutexGuard coming from?

Here's what the rust-lang documentation says, of the lock method:

The portion of Mutex.lock() from Rust API doc

It returns a LockResult, which when transformed using an unwrap() like in the code above, produces a MutexGuard which is guarding an i32 (the type T) in this case.

What it we try to acquuire the lock again, on the same a_mutex?

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.lock().unwrap();
    println!("Good bye!");
}
Enter fullscreen mode Exit fullscreen mode

This time, the output is:

guard -> 5
Mutex itself -> Mutex { data: <locked>, poisoned: false, .. }
Enter fullscreen mode Exit fullscreen mode

The program never terminates (it cannot even say 'Good Bye')! Why? This is what the documentation says:

The exact behavior on locking a mutex in the thread which already holds the lock is left unspecified. However, this function will not return on the second call (it might panic or deadlock, for example).

In our case, we made a second call, on a mutex, in the same thread (the main thread here, there is only one)!

Mutex has a helpful alternative, in the form of Mutex::try_lock(), which allows us to attempt to acquire the lock, knowing that we may fail.

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

This time, the output is:

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

The error message is quite explanatory. The attempt to acquire the lock again, fails because the it will have caused a forever blocking and therefore, non-termination of the program as we have seen eariler.

The point of interest is this part from rust-lang documentation:

This function will block the local thread until it is available to acquire the mutex. Upon returning, the thread is the only thread with the lock held. An RAII guard is returned to allow scoped unlock of the lock. When the guard goes out of scope, the mutex will be unlocked.

The type of this RAII guard is MutexGuard, (my comment on the code-snippet). What is its purpose in life?

Again, rust-lang documentation tells us in comforting details:

An RAII implementation of a “scoped lock” of a mutex. When this structure is dropped (falls out of scope), the lock will be unlocked.
The data protected by the mutex can be accessed through this guard via its Deref and DerefMut implementations.
This structure is created by the lock and try_lock methods on Mutex.

The key portion, in our context, is this: "When this structure is dropped (falls out of scope), the lock will be unlocked".

In search of the 'Scope'

Let us try and find out what is the scope of the guard.

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

    let a_mutex = Mutex::new(5);

    {   // <-- Beginning of scope
        let guard = a_mutex.lock().unwrap(); // <-- 'guard' is of type `MutexGuard`
        println!("guard -> {:?}", guard);
    }   // <-- End of scope`

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

The output is quite as expected.

guard -> 5
Mutex itself -> Mutex { data: 5, poisoned: false, .. }
Good bye!
Enter fullscreen mode Exit fullscreen mode

Don't miss the fact that Mutex { data: 5, poisoned: false, .. } indicates the absence of the lock being held, as opposed to the output that we have seen earlier: Mutex { data: <locked>, poisoned: false, .. }. The following achieves the same effect, for the same reason:

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

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

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

The output is exactly the same as the precding output ( no lock ).

We establish then, that when the guard is dropped, the lock is released automatically, as per the premise of RAII (or OBRM, if you like). But, when is it constructed exactly?

Let's be a bit adventurous and peep into the implementation of std::sync::Mutex.rs:

#[stable(feature = "rust1", since = "1.0.0")]
    pub fn lock(&self) -> LockResult<MutexGuard<'_, T>> {
        unsafe {
            self.inner.lock();
            MutexGuard::new(self)
        }
    }
Enter fullscreen mode Exit fullscreen mode

Let's ignore everything except, the return type and the call to MutexGuard::new(self). Even without much knowledge about the internals, it is quite clear that when Mutex::lock() returns, it carries with itself, freshly built MutexGuard object. Just before that happens, a lock resource (Operating System resource) has been obtained. The initialization has guaranteed the acquisition!

How about release of the lock? Again, peeping into the source of std::sync::Mutex.rs:

#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Drop for MutexGuard<'_, T> {
    #[inline]
    fn drop(&mut self) {
        unsafe {
            self.lock.poison.done(&self.poison);
            self.lock.inner.unlock();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The key point for us is that self.lock.inner.unlock() statement. As the MutexGuard is dropped, the lock is released!

Therefore, the scope in which a MutexGuard remains valid, follows the idiom of RAII!

The temporary MutexGuard

The preceding article in this series discusses the concept of temporary variable. One of the takeaways from that is the presence of a receiver, which is temporarily produced to complete the method-call expression.

The concept of this temporary object is applicable in the case of MutexGuard here too. And, following the scoping rules of a temporary, the MutexGuard ceases to exist at the end of the statement.

fn main() {

    use std::sync::Mutex;

    let a_mutex = Mutex::new(5);
    let guard = a_mutex.lock().unwrap().to_string(); // <-- 'guard' is of type `String`

    println!("Mutex itself -> {:?}", a_mutex); // <-- MutexGuard is not being held
    println!("guard -> {:?}", guard);

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

Nothing surprising about the output:

guard -> "5"
Mutex itself -> Mutex { data: 5, poisoned: false, .. }
Good bye!
Enter fullscreen mode Exit fullscreen mode

Notice the absence of any data: <locked> in the Mutex. When the preceding statement ends - yielding the stringified representation of value '5' - the tempoary MutexGuard

  • reaches the end of its scope,
  • is dropped, and therefore
  • releases the lock (re: RAII)

We have read the value that is fenced in by the Mutex and have trodden in the world of expressions along with temporary objects. In the next article in this series, we explore more about how the scope of the temporaries is affected by the body of an expression.

Main Takeaways

  • Mutex follows the idiom of RAII (or OBRM) for ensuring that access to the value it is guarding, cannot be breached.
  • Mutex.lock() produces a temporary MutexGuard whose construction and destruction ( Drop ), acquires and releases the lock, in that order.
  • Because it is a temporary object, the MutexGuard is dropped at the end of the statement that creates it, unless it scope is expanded by other means (that is not a part of this article though).
💖 💪 🙅 🚩
nsengupta
Nirmalya Sengupta

Posted on June 16, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related