Is there a facility to lock multiple mutexes in Rust while preventing deadlocking?
Asked Answered
P

3

19

Is there a C++ std::lock() like facility in Rust to prevent deadlocking in code like this:

type Type0 = Arc<Mutex<u8>>;
type Type1 = Arc<Mutex<u16>>;

fn foo(a: Type0, b: Type1) {
    let a_guard = a.lock().unwrap();
    let b_guard = b.lock().unwrap();
}

fn bar(a: Type0, b: Type1) {
    let b_guard = b.lock().unwrap();
    let a_guard = a.lock().unwrap();
}

If foo is called by thread-0 and bar by thread-1 there is a chance of deadlock. Is there something, hopefully variadic because I can have more than 2, to help me with this or am I all on my own verifying the correctness of order of locking?

From the documentation for std::lock:

Locks the given Lockable objects lock1, lock2, ..., lockn using a deadlock avoidance algorithm to avoid deadlock.

Progress answered 8/9, 2016 at 14:1 Comment(1)
Admittedly I don't know how std::lock works, but (unless you follow mmstick's answer in lumping the data together and locking/unlocking it at once) I don't see how you could ever guarantee no deadlocks. Sure, you could write a macro to do them in the right order -- but then you've just mutated the problem of checking that you always lock them in the right order into the barely-easier problem of checking that you always use the macro and never call .lock() directly. Right?Optional
O
8

No, Rust does not have a function equivalent to C++'s std::lock.

Based on the fact that it doesn't seem to be in the std::sync documentation and Googling brings up nothing useful, I'm pretty confident in this assertion.

Why not? Well, if I may editorialize a bit, std::lock isn't as broadly useful as you might hope. Deadlock avoidance is nontrivial and every algorithm will have plausible corner cases that result in poor performance or even livelock. There is no one-size-fits-all deadlock avoidance algorithm.¹ (See Is std::lock() ill-defined, unimplementable, or useless?) Putting a deadlock-avoiding lock function in the standard library suggests that it's a good default choice, and perhaps encourages its use without regard for its implementation. Most real-life applications probably would do as well with a simpler (and less general-purpose) algorithm.

There are crates that provide deadlock avoidance by other means. For instance, tracing-mutex provides locking types that create a dependency graph at runtime and will panic instead of deadlocking if the dependency graph contains a cycle. parking_lot has an experimental deadlock_detection feature (but I'm not sure how it works). Curiously, I didn't find any crates that provide a C++ std::sort equivalent with backing off.

Anyway, nothing stops you writing your own "backing off" algorithm to solve this problem; it's just not part of the standard library.


¹ In fairness, you could make the same argument for other functions Rust does have, like [T]::sort. But there are many applications where sorting is not a bottleneck and any reasonably fast algorithm is good enough. Deadlock avoidance is both less likely to be necessary in general, and more likely to be performance-sensitive when it does appear.

Optional answered 10/9, 2016 at 17:24 Comment(0)
E
6

This is easily solved if the Mutex is a tuple with the contained values, so that locking the tuple locks both values simultaneously.

let tuple_mutex = Arc::new(Mutex::new((A, B)));
Expostulation answered 8/9, 2016 at 14:47 Comment(4)
Excellent point, but might want to retain the flexibility of independent values.Odell
We can view tuple is just a single type - so that is basically Mutexing a single type. What i am looking for though is a solution to different Mutexs. For granularity you might not want to always lump them into a single tuple/type - then you are sacrificing cases where they could function independent of each other which abound in my code. It's really a difference betwen Arc<Mutexing the whole struct vs Arc<Mutexing individual fields. The latter gives much finer control and my question was about that - is there a facility similar to std::lock of c++.Progress
Does this really allow us to lock the mutextes simultaneously, or are they in reality locked one after the other? If so, what order are the mutextes unlocked in?Rockwell
@SimonElliott There is only one Mutex in this solution; it is used to lock a compound value. The values are locked simultaneously because they are controlled by the same mutex.Optional
F
0

I ran into this issue where I have a mutex for every logged in user. When two users interact, I need to lock both users. To solve this I needed some sorting mechanism to order the locks. Since I'm using Arc<Mutex<User>> I wrote a utility function to handle it:

async fn ordered_lock<'a, T>(
    m1: &'a Arc<Mutex<T>>,
    m2: &'a Arc<Mutex<T>>,
) -> (MutexGuard<'a, T>, MutexGuard<'a, T>) {
    let l1;
    let l2;
    if Arc::as_ptr(m1) > Arc::as_ptr(m2) {
        l2 = m2.lock().await;
        l1 = m1.lock().await;
    } else {
        l1 = m1.lock().await;
        l2 = m2.lock().await;
    };
    (l1, l2)
}

It'd work the same with non-async mutex,

Franchot answered 10/9, 2023 at 1:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.