[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:
-
Rust Notes on Temporary values (usage of Mutex) - 1 (link)
Explores Method-call expressions and binding.
-
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!");
}
a_mutex.lock().unwrap().to_string()
: In that expression, multiple steps are taking place:
- Lock is obtained from
a_mutex
. An unnamedMutexGuard
object is produced. - The
MutexGuard
is unwrapped and its internali32
( value of '5') is made available (through a mechanism of deref but that is not important here) - The
to_string()
method available oni32
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!");
}
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
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!");
}
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!
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!");
}
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);
}
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);
}
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!");
}
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
, alet
statement forces the guard to exist for longer (depends on the scope of thelet
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 thematch
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)