Why would I want to lock two mutexes in one functi

2019-08-18 03:51发布

问题:

https://en.cppreference.com/w/cpp/thread/lock_tag

void transfer(bank_account &from, bank_account &to, int amount)
{
    // lock both mutexes without deadlock
    std::lock(from.m, to.m);
    // make sure both already-locked mutexes are unlocked at the end of scope
    std::lock_guard<std::mutex> lock1(from.m, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(to.m, std::adopt_lock);

// equivalent approach:
//    std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
//    std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
//    std::lock(lock1, lock2);

    from.balance -= amount;
    to.balance += amount;
}

What do they gain by locking two mutexes at once?
What have they gained by defered lock here?

Please explain the reason behind that decision of theirs.

回答1:

Extension on Richard Hodges's answer

What do they gain by locking two mutexes at once?

Richard explained nicely already, just a little bit more explicit: we avoid dead-lock this way (std::lock is implemented such that dead-lock won't occur).

What have they gained by deferred lock here?

Deferring the lock results in not acquiring it immediately. That's important because if they did so, they would just do it without any protection against dead-lock (which the subsequent std::lock then achieves).

About dead lock avoidance (see std::lock):

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

The objects are locked by an unspecified series of calls to lock, try_lock, and unlock. [...]

Side note: another, much simpler algorithm avoiding dead locks is always locking the bank account with e. g. lower account number (AN) first. If a thread is waiting for the lock of higher AN, then the other thread holding it either already has both of the locks acquired or is waiting for the second – which cannot be the one of the first thread as it must have a yet higher AN.

This does not change much for arbitrary number of threads, any thread holding a lower lock is waiting for a higher one, if hold as well. If you draw a directed graph with edges from A to B if A is waiting for second lock that B holds, you'll get a (multi-) tree structure, but you won't ever have circular substructures (which would indicate a dead lock).



回答2:

If I modify a bank account without locking it, someone else could try to modify it at the same time. This is a race and the result will be undefined behaviour (usually lost or magically created money).

While transferring money, I am modifying 2 bank accounts. So they both need to be locked.

The problem is that when locking more than one thing, every locker must lock and unlock in the same order, otherwise we get deadlocks.

When it's bank accounts, there is no natural order of locks. Thousands of threads could be transferring money in all directions.

So we need a method of locking more than one mutex in a way that works around this - this is std::lock

std::lock merely locks the mutex - it does not guarantee unlocking on exit from the current code block.

std::lock_guard<> unlocks the mutex it's referring to upon destruction (see RAII). This makes the code behave correctly in all circumstances - even when there is an exception which might cause an early exit from the current code block without the code flowing over statement such as to.m.unlock()

A good explanation (with examples) here: https://wiki.sei.cmu.edu/confluence/display/cplusplus/CON53-CPP.+Avoid+deadlock+by+locking+in+a+predefined+order



回答3:

The bank account data structure has a lock for each account.

When transferring money from one account to another, we need to lock both accounts (since we are removing money from one and adding it to another). We would like this operation not to deadlock, so lock both at once using std::lock, since doing it like that ensures there isn't a deadlock.

After we finish the transaction we need to make sure we release the lock. This code does that using RAII. With the adopt_lock tag, we make the object adopt an already locked mutex (which will be released when lock1 falls out of scope). With the defer_lock tag, we create a unique_lock for a currently unlocked mutex, with the intention of locking it later. Again, it will be unlocked when the unique_lock falls out of scope.



回答4:

from and to are 2 accounts which may be used anywhere in the application separatelly.

By having mutex for each account, you make sure nobody uses from nor to accounts while you do the transfer.

lock_guard will release mutex when exiting from function.



标签: c++ std mutex