Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Where is a MutexGuard if I never assign it to a variable?

I don't understand "where" the MutexGuard in the inner block of code is. The mutex is locked and unwrapped, yielding a MutexGuard. Somehow this code manages to dereference that MutexGuard and then mutably borrow that object. Where did the MutexGuard go? Also, confusingly, this dereference cannot be replaced with deref_mut. Why?

use std::sync::Mutex;

fn main() {
    let x = Mutex::new(Vec::new());
    {
        let y: &mut Vec<_> = &mut *x.lock().unwrap();
        y.push(3);
        println!("{:?}, {:?}", x, y);
    }

    let z = &mut *x.lock().unwrap();
    println!("{:?}, {:?}", x, z);
}
like image 422
AnimatedRNG Avatar asked Jul 14 '18 05:07

AnimatedRNG


2 Answers

Summary: because *x.lock().unwrap() performs an implicit borrow of the operand x.lock().unwrap(), the operand is treated as a place context. But since our actual operand is not a place expression, but a value expression, it gets assigned to an unnamed memory location (basically a hidden let binding)!

See below for a more detailed explanation.


Place expressions and value expressions

Before we dive in, first two important terms. Expressions in Rust are divided into two main categories: place expressions and value expressions.

  • Place expressions represent a value that has a home (a memory location). For example, if you have let x = 3; then x is a place expression. Historically this was called lvalue expression.
  • Value expressions represent a value that does not have a home (we can only use the value, there is no memory location associated with it). For example, if you have fn bar() -> i32 then bar() is a value expression. Literals like 3.14 or "hi" are value expressions too. Historically these were called rvalue expressions.

There is a good rule of thumb to check if something is a place or value expression: "does it make sense to write it on the left side of an assignment?". If it does (like my_variable = ...;) it is a place expression, if it doesn't (like 3 = ...;) it's a value expression.

There also exist place contexts and value contexts. These are basically the "slots" in which expressions can be placed. There are only a few place contexts, which (usually, see below) require a place expression:

  • Left side of a (compound) assignment expression (⟨place context⟩ = ...;, ⟨place context⟩ += ...;)
  • Operand of an borrow expression (&⟨place context⟩ and &mut ⟨place context⟩)
  • ... plus a few more

Note that place expressions are strictly more "powerful". They can be used in a value context without a problem, because they also represent a value.

(relevant chapter in the reference)

Temporary lifetimes

Let's build a small dummy example to demonstrate a thing Rust does:

struct Foo(i32);

fn get_foo() -> Foo {
    Foo(0)
}

let x: &Foo = &get_foo();

This works!

We know that the expression get_foo() is a value expression. And we know that the operand of a borrow expression is a place context. So why does this compile? Didn't place contexts need place expressions?

Rust creates temporary let bindings! From the reference:

When using a value expression in most place expression contexts, a temporary unnamed memory location is created initialized to that value and the expression evaluates to that location instead [...].

So the above code is equivalent to:

let _compiler_generated = get_foo();
let x: &Foo = &_compiler_generated;

This is what makes your Mutex example work: the MutexLock is assigned to a temporary unnamed memory location! That's where it lives. Let's see:

&mut *x.lock().unwrap();

The x.lock().unwrap() part is a value expression: it has the type MutexLock and is returned by a function (unwrap()) just like get_foo() above. Then there is only one last question left: is the operand of the deref * operator a place context? I didn't mention it in the list of place contests above...

Implicit borrows

The last piece in the puzzle are implicit borrows. From the reference:

Certain expressions will treat an expression as a place expression by implicitly borrowing it.

These include "the operand of the dereference operator (*)"! And all operands of any implicit borrow are place contexts!

So because *x.lock().unwrap() performs an implicit borrow, the operand x.lock().unwrap() is a place context, but since our actual operand is not a place, but a value expression, it gets assigned to an unnamed memory location!

Why doesn't this work for deref_mut()

There is an important detail of "temporary lifetimes". Let's look at the quote again:

When using a value expression in most place expression contexts, a temporary unnamed memory location is created initialized to that value and the expression evaluates to that location instead [...].

Depending on the situation, Rust chooses memory locations with different lifetimes! In the &get_foo() example above, the temporary unnamed memory location had a lifetime of the enclosing block. This is equivalent to the hidden let binding I showed above.

However, this "temporary unnamed memory location" is not always equivalent to a let binding! Let's take a look at this case:

fn takes_foo_ref(_: &Foo) {}

takes_foo_ref(&get_foo());

Here, the Foo value only lives for the duration of the takes_foo_ref call and not longer!

In general, if the reference to the temporary is used as an argument for a function call, the temporary lives only for that function call. This also includes the &self (and &mut self) parameter. So in get_foo().deref_mut(), the Foo object would also only live for the duration of deref_mut(). But since deref_mut() returns a reference to the Foo object, we would get a "does not live long enough" error.

That's of course also the case for x.lock().unwrap().deref_mut() -- that's why we get the error.

In the deref operator (*) case, the temporary lives for the enclosing block (equivalent to a let binding). I can only assume that this is a special case in the compiler: the compiler knows that a call to deref() or deref_mut() always returns a reference to the self receiver, so it wouldn't make sense to borrow the temporary for only the function call.

like image 65
Lukas Kalbertodt Avatar answered Oct 15 '22 18:10

Lukas Kalbertodt


Here are my thoughts:

let y: &mut Vec<_> = &mut *x.lock().unwrap();

A couple of things going on under the surface for your current code:

  1. Your .lock() yields a LockResult<MutexGuard<Vec>>
  2. You called unwrap() on the LockResult and get a MutexGuard<Vec>
  3. Because MutexGuard<T> implements the DerefMut interface, Rust performs deref coercion. It gets dereferenced by the * operator, and yields a &mut Vec.

In Rust, I believe you don't call deref_mut by your own, rather the complier will do the Deref coercion for you.

If you want to get your MutexGuard, you should not dereference it:

let mut y  = x.lock().unwrap();
(*y).push(3);
println!("{:?}, {:?}", x, y);
//Output: Mutex { data: <locked> }, MutexGuard { lock: Mutex { data: <locked> } }

From what I have seen online, people usually do make the MutexGuard explicit by saving it into a variable, and dereference it when it is being used, like my modified code above. I don't think there is an official pattern about this. Sometimes it will also save you from making a temporary variable.

like image 29
Xu Chen Avatar answered Oct 15 '22 18:10

Xu Chen