3

I was researching possible implementations for the std::lock function and stumbled upon an implementation posted on the code review community.

Quoting the accepted answer (emphasis mine):

No this does not meet the definition of std::lock().

It (std::lock) guarantees that no matter what order you specify the locks in the parameter list you will not fall into a deadlock situation.

[...]

This also means that if a lock in the list is already locked it must be released so that the locks are acquired in the correct order.

I cannot find a conclusive answer whether or not the last statement is correct.

My question: is it allowed (i.e. defined behavior) to pass a locked resource, owned by the calling thread, as an argument to the standard std::lock function?

std::mutex m1, m2;
m1.lock();
std::lock(m1, m2);

My gut feeling says this is actually not allowed. The function expects two or more Lockable objects and there is no way to check if a Lockable object is already locked by the current thread of execution. So it seems impossible to implement std::lock that way.

Maarten Bamelis
  • 2,243
  • 19
  • 32

3 Answers3

3

Is it allowed (i.e. defined behavior) to pass a locked resource?

No. It is not. I just tested this out and I got a deadlock. Both mutexes passed to std::lock have to be freed. If they are recursive mutexes, then, they can be already locked by the current thread. Otherwise, if they were locked by a different thread, you will get a deadlock.

If you know when the mutexes are going to be locked, you can use a custom lockable object to only lock one of the mutexes, for example:

class CustomDualLock {
    bool first_time;
    std::mutex& _mutex1;
    std::mutex& _mutex2;

public:
    CustomDualLock(std::mutex& mutex1, std::mutex& mutex2)
        : first_time(true),
        _mutex1(mutex1), 
        _mutex2(mutex2) {
        lock();
    }

    ~CustomDualLock() {
        unlock();
    }

    CustomDualLock(const CustomDualLock&) = delete;
    CustomDualLock& operator =(const CustomDualLock&) = delete;
    CustomDualLock(const CustomDualLock&&) = delete;
    CustomDualLock& operator =(const CustomDualLock&&) = delete;

    void lock() {
        if( first_time ) {
            first_time = false;
            _mutex1.lock();
        } 
        else {
            std::lock(_mutex1, _mutex2);
        }
    }

    void unlock() {
        _mutex1.unlock(); 
        _mutex2.unlock();
    }
};

Update

I just found something being more clear about the behavior (it was talking about scoped locks (C++ 17), which calls std::lock when more than 1 lock is passed): https://en.cppreference.com/w/cpp/thread/scoped_lock/scoped_lock

The behavior is undefined if one of MutexTypes is not a recursive mutex and the current thread already owns the corresponding argument in m...

As it says, the behavior is undefined. On my compiler (GCC 8.4) at least, I got a deadlock. But may be in some other compiler, I may not.

References:

  1. Using more than one mutex with a conditional variable
  2. Condition variable waiting on multiple mutexes
  3. What is the best way to wait on multiple condition variables in C++11?
  4. https://en.cppreference.com/w/cpp/thread/lock
  5. https://en.cppreference.com/w/cpp/thread/condition_variable_any
Evandro Coan
  • 8,560
  • 11
  • 83
  • 144
2

My local draft of the standard says of lock, in 30.4.3/5

Effects: All arguments are locked via a sequence of calls to lock(), try_lock(), or unlock() on each argument. The sequence of calls shall not result in deadlock, but is otherwise unspecified. [ Note: A deadlock avoidance algorithm such as try-and-back-off must be used, but the specific algorithm is not specified to avoid over-constraining implementations. — end note ] If a call to lock() or try_lock() throws an exception, unlock() shall be called for any argument that had been locked by a call to lock() or try_lock().

So, it's clear that it may release locks acquired while working, but it doesn't say anything about whether locks held before entry may be have been released when it exits.

Presumably, so long as either

  1. it succeeds and all lockables are locked, or
  2. it throws, and exactly those lockables previously locked by this thread still are (no previously un-held locks held, and no previously-locked items now released)

it shouldn't make any difference what happens inside. Note that the language "... a sequence of calls ... on each argument" certainly seems to allow calling unlock on something locked before entry.

Useless
  • 64,155
  • 6
  • 88
  • 132
  • So a generic implementation that must work on any Lockable object must assume none of them are already locked by the calling thread. I could pass a mutex, which is a Lockable object, and attempting to lock a mutex held by the current thread is undefined behavior. But a specialization could be provided for Lockable objects that expose their state, e.g. for `std::unique_lock`, which has the `owns_lock` method, that exploit this extra information. I suppose that is not too weird! – Maarten Bamelis Dec 04 '17 at 19:13
1

I guess you ask your questing regarding a second std::lock in the thread on the mutex that is already locked in the same thread before. If an already locked resource is recursive_mutex, it is allowed. If it is a general mutex, you reach a deadlock.

273K
  • 29,503
  • 10
  • 41
  • 64
  • Your answer made it clear to me that I had to phrase my question in more detail. But you deduced what I meant to ask: the scenario is indeed locking a mutex and then passing it as an argument to `std::lock`. I have updated my question and added an example snippet. – Maarten Bamelis Dec 04 '17 at 19:03
  • You quoted the part of the standard regarding std::lock, in which std::try_lock is mentioned. Why you do not continue read the standard regarding std::try_lock? The standard says that std::try_lock attempts to lock the mutex, without blocking and in the final (Sorry, I can not input new line in mobile web version): - If the mutex is currently locked by the same thread calling this function, it produces a deadlock (with undefined behavior). See recursive_mutex for a mutex type that allows multiple locks from the same thread. – 273K Dec 05 '17 at 00:45