11

The following code contains a potential deadlock, but seems to be necessary: to safely copy data to one container from another, both containers must be locked to prevent changes from occurring in another thread.

void foo::copy(const foo & rhs)
{
    pMutex->lock();
    rhs.pMutex->lock();
    // do copy
}

Foo has an STL container and "do copy" essentially consists of using std::copy. How do I lock both mutexes without introducing deadlock?

pomeroy
  • 1,377
  • 1
  • 12
  • 21
  • `std::lock` has a deadlock-avoidance algorithm pass it both mutexes and it will be more readable to other's than implementing your own. – Mellester Feb 16 '19 at 21:27

6 Answers6

16

Impose some kind of total order on instances of foo and always acquire their locks in either increasing or decreasing order, e.g., foo1->lock() and then foo2->lock().

Another approach is to use functional semantics and instead write a foo::clone method that creates a new instance rather than clobbering an existing one.

If your code is doing lots of locking, you may need a complex deadlock-avoidance algorithm such as the banker's algorithm.

Greg Bacon
  • 134,834
  • 32
  • 188
  • 245
  • 1
    Even something as simple as the addresses of this vs rhs would work. always lock the one with the lower address first. – Kyle Butt Feb 12 '10 at 04:24
  • clone would only work well if it weren't copying, and I don't think implicit sharing will work, but I'll take a look. Interesting approach Kyle. I can't see any flaws. – pomeroy Feb 12 '10 at 20:49
  • it's a nice solution to suggest to him to make a temporary copy of the data. `std::lock` already offers such a deadlock-avoidance algorithm however. and would be more readable – Mellester Feb 16 '19 at 21:23
1

How about this?

void foo::copy(const foo & rhs)
{
    scopedLock lock(rhs.pMutex); // release mutex in destructor
    foo tmp(rhs);
    swap(tmp); // no throw swap locked internally
}

This is exception safe, and pretty thread safe as well. To be 100% thread save you'll need to review all code path and than re-review again with another set of eyes, after that review it again...

Shing Yip
  • 1,596
  • 1
  • 12
  • 11
1

As @Mellester mentioned you can use std::lock for locking multiple mutexes avoiding deadlock.

#include <mutex>

void foo::copy(const foo& rhs)
{
    std::lock(pMutex, rhs.pMutex);

    std::lock_guard<std::mutex> l1(pMutex, std::adopt_lock);
    std::lock_guard<std::mutex> l2(rhs.pMutex, std::adopt_lock);

    // do copy
}

But note to check that rhs is not a *this since in this case std::lock will lead to UB due to locking same mutex.

4xy
  • 3,494
  • 2
  • 20
  • 35
  • Any reason why you wouldn't do `std::unique_lock` first with `std::defer_lock` and then perform `std::lock(l1,l2);` ? Or is it just personal preference / style? – Gizmo Apr 23 '21 at 09:30
0

this is a known problem already there is a std solution. std::lock() can be called on 2 or more mutex at the same time whilst avoiding deadlock's. More information here it does offer a recommendation.

std::scoped_lock offers a RAII wrapper for this function, and is generally preferred to a naked call to std::lock.

of course this doesn't really allow early releases of one lock above the other so use std::defer_lock or std::adopt_lock like I did in this answer to a similar question.

Mellester
  • 922
  • 7
  • 9
-1

To avoid a deadlock its probably best, to wait until both resources can be locked:

Dont know which mutex API you are using so here is some arbitrary pseudo code, assume that can_lock() only checks if it can lock a mutex, and that try_lock() returns true if it did lock, and false, if the mutex is already locked by somebody else.

void foo::copy(const foo & rhs)
{
    for(;;)
    {
        if(! pMutex->cany_lock() || ! rhs.pMutex->cany_lock())
        {
            // Depending on your environment call or dont call sleep()
            continue;
        }
        if(! pMutex->try_lock())
            continue;
        if(! rhs.pMutex->try_lock())
        {
            pMutex->try_lock()
            continue;
        }
        break;
    }
    // do copy
}
RED SOFT ADAIR
  • 12,032
  • 10
  • 54
  • 92
  • 4
    To avoid a deadlock, it's best to introduce a livelock? And spin, using 100% CPU? – bk1e Feb 13 '10 at 05:17
-2

You can try locking both the mutexes at the same time using scoped_lock or auto_lock.... like bank transfer do...

void Transfer(Receiver recv, Sender send)
{
    scoped_lock rlock(recv.mutex);
    scoper_lock slock(send.mutex);

    //do transaction.
}
Naveen
  • 177
  • 2
  • 15