How does "{:?}" read value wrapped inside Arc<Mutex> without acquiring a lock?

let a = Mutex::new(1);
println!("{:?}", a) // this prints that Mutex has a value 1
let a = Mutex::new(1);
a.lock(); //a.lock().await if using e.g. Tokio
println!("{:?}", a) // This also lets us know that Mutex has a value of 1 inside

I'm trying to peek value wrapped inside the mutex while not acquiring a lock. I have no intent to mutate the value, just "seeing". I learned that RwLock might allow us to do it but also have discovered that "debug formatter" does seem to access the value inside Mutex. It does so even when the lock has been taken.

Could someone explain why/how this can happen? Would there be a way to see the value inside the mutex without getting the lock?

EDIT
I missed one important thing. I've been actually dealing with Arc, as in

let a = Arc::new(Mutex::new(1));
let c = a.clone()
println!("{:?}", c);

The compiler warns that lock() returns a Result:

warning: unused `Result` that must be used
 --> src/main.rs:5:5
  |
5 |     a.lock();
  |     ^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: this `Result` may be an `Err` variant, which should be handled

If you fix that with a.lock().unwrap() then it tells you:

warning: unused `MutexGuard` that must be used
 --> src/main.rs:5:5
  |
5 |     a.lock().unwrap();
  |     ^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: if unused the Mutex will immediately unlock

You need to save the lock guard to keep the mutex locked. Otherwise it's only a temporary value and is dropped when the statement ends.

use std::sync::Mutex;

fn main() {
    let a = Mutex::new(1);
    let _lock = a.lock().unwrap();
    println!("{:?}", a);
}

This outputs:

Mutex { data: <locked>, poisoned: false, .. }

Now the mutex is locked and can't be read.


By the way, looking at the standard library's source is a great way to see what's going on under the covers. You can see in Mutex's source code that it does indeed try to lock itself when printing:

impl<T: ?Sized + fmt::Debug> fmt::Debug for Mutex<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut d = f.debug_struct("Mutex");
        match self.try_lock() {
            ...
        }
        ...
    }
}
1 Like

The whole point of the lock is specifically so that others can't see any mutations that might be happening concurrently. You have to lock the mutex to ensure nobody else is currently mutating it. There's no way to read the value without locking it, and the mutex would arguably not be doing its job if you could.

You can use RwLock if you want multiple concurrent readers, but they will all still all have to lock it to ensure there are no concurrent writers.

The debug formatter prints the value by attempting to lock it first.

2 Likes

I edited the original post. I was actually using Mutex with "Arc". Would this make a difference?

let b = std::sync::Arc::new(std::sync::Mutex::new(1));
let c = b.clone();
println!("44 {:?}", c)
44 Mutex { data: 1, poisoned: false, .. }

It somehow read data 1, which got me intrigued. But it looks like there's no way to actually access the field data ?

The Arc doesn't change anything. Locking is the same:

let _lock = c.lock().unwrap();
println!("{:?}", c);
2 Likes

Did the homework. Looks like mutex internally tries to get the lock. Appreciate the comments! Closing this issue.

#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized + fmt::Debug> fmt::Debug for Mutex<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut d = f.debug_struct("Mutex");
        match self.try_lock() {
            Ok(guard) => {
                d.field("data", &&*guard);
            }
            Err(TryLockError::Poisoned(err)) => {
                d.field("data", &&**err.get_ref());
            }
            Err(TryLockError::WouldBlock) => {
                struct LockedPlaceholder;
                impl fmt::Debug for LockedPlaceholder {
                    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                        f.write_str("<locked>")
                    }
                }
                d.field("data", &LockedPlaceholder);
            }
        }
        d.field("poisoned", &self.poison.get());
        d.finish_non_exhaustive()
    }
}

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.