Crate for locking by name

There's probably a crate for this, but I've been looking through all the crates with "lock" in their name and haven't found it . I need a mutex-type lock like

let lok = LockByName::new("lockthisname"); // anything else wanting "lockthisname" will lock
....
// lok gets dropped, lock is unlocked.

Suggestions?

What's your use case?

Prevent fetching the same named asset from a server twice from different threads, before it arrives in the local cache and prevents further fetches.

I've implemented this a few times, but I don't know where the implementations are. I'm not sure that there is a crate for it.

Here's something quick and dirty just using the standard library and once_cell. (Not well tested :sweat_smile:)

mod mutex {
    use std::collections::HashMap;
    use std::sync::{Arc, Mutex};

    use once_cell::sync::OnceCell;

    type Globals = Mutex<HashMap<&'static str, Arc<Mutex<()>>>>;

    pub fn get(name: &'static str) -> Arc<Mutex<()>> {
        static GLOBALS: OnceCell<Globals> = OnceCell::new();
        GLOBALS
            .get_or_init(|| Mutex::new(HashMap::new()))
            .lock()
            .unwrap()
            .entry(name)
            .or_insert_with(|| Arc::new(Mutex::new(())))
            .clone()
    }
}

// Usage
{
    let m = mutex::get("lockthisname");
    let _guard = m.lock().unwrap();
    // ....
    // `_guard` gets dropped, `lockthisname` is unlocked
}

The tricky part is that you want to remove it from the map again afterwards. Otherwise you have what's essentially a memory leak.

Yeah that's true, its an interesting problem.

Another solution that might work for this use case (fetching named assets to a local cache) could be to use the fs2 crate on the local file path. Unfortunately, I'm not sure how well maintained that crate is.

Need to return a structure which has a Drop function to do the unlock and remove.

I think this is free of race conditions. But I'm not sure. Comments?

I guess you can simply use the flock related crate?

This isn't related to the file system. The keys are UUIDs of assets on a virtual world server.

Unfortunately this doesn't work:

If a LockByName is dropped while another is locked then you can acquire a new (different) Arc to the same name. I think in order to get this to work the LockByName needs to store a MutexGuard

Yes, I also thought LockByName needs to store the MutexGuard.

But, for some reason, it's a temporary and can't be returned. What's wrong there?

struct LockByName<'a> {
    name: &'static str,
    lok: Arc<Mutex<()>>,
    guard: MutexGuard<'a, ()>
}

// ...

        let guard = lok.clone().lock().unwrap(); // locks here
        LockByName { lok, name, guard } // item for drop

This looks like a self-referential struct, which will definitely cause problems. You have to store the MutexGuard separately from the Mutex.

It's not that it's self-referential. It's closer to a diamond pattern. guard references lok. The returned LockByName references both. Without the clone() call, that's re-using a mutable borrow. With the clone call, though, since lok is an Arc, it seemingly should work. It's not a loop. But no use of clone seems to fix this.

Calling .lock on a clone crates a guard that borrows from the clone. The compiler thus requires the clone to outlive the guard. However the clone is a temporary that is freed by the end of the statement.

If the compiler allowed this, then there's no other way it could prevent the following:

let LockByName { lok, name, guard } = lock;
drop(lok);

// If lok was the last copy then the refcount
// is now zero and the mutex has been freed
*guard; // invoke UB

"Reuse of a mutable borrow" is not the reason why it is impossible to put a mutex and a guard for it on the same struct. After all, as far as the borrow checker is concerned, the mutex has only been borrowed immutably.

This happens with all form of "thing, with another thing that borrows from it".

1 Like

This is harder than I expected. Any ideas?

2 Likes

This doesn't seem to block when the same key is locked twice.

You have to call NamedLock::lock.

I approached this by storing Condvars instead of Mutexes in the map: Rust Playground

Output:

(main) Locked 'lockthis.
(main) Locked 'lockthat.
Unlocked 'lockthat (1 waiting)
Notified 'lockthat
(child) Locked 'lockthat.
Unlocked 'lockthat (0 waiting)
Deleted 'lockthat
Unlocked 'lockthis (0 waiting)
Deleted 'lockthis

Edit: Here's a slight variant that uses notify_one and lets the OS decide which thread to wake: Rust Playground