4

I was wondering what happens when you move a unique_lock that holds a recursive_mutex.

Specifically, I was looking at this code:

recursive_mutex g_mutex;

#define TRACE(msg) trace(__FUNCTION__, msg)

void trace(const char* function, const char* message)
{
    cout << std::this_thread::get_id() << "\t" << function << "\t" << message << endl;
}

future<void> foo()
{
    unique_lock<recursive_mutex> lock(g_mutex);
    TRACE("Owns lock");
    auto f = std::async(launch::async, [lock = move(lock)]{
        TRACE("Entry");
        TRACE(lock.owns_lock()? "Owns lock!" : "Doesn't own lock!"); // Prints Owns lock! 
        this_thread::sleep_for(chrono::seconds(3));
    });
    TRACE(lock.owns_lock()? "Owns lock!" : "Doesn't own lock!"); // Prints Doesn't own lock! 
    return f;
}


int main()
{
    unique_lock<recursive_mutex> lock(g_mutex);
    TRACE("Owns lock");
    auto f = foo();    
    TRACE(lock.owns_lock()? "Owns lock!" : "Doesn't own lock!");        // Prints Owns lock! 
    f.wait();
    TRACE(lock.owns_lock()? "Owns lock!" : "Doesn't own lock!");        // Prints Owns lock!
}

The output of this sample code surprised me a lot. How does the unique_lock in main() know that the thread released the mutex? Is it real?

Mr. Anderson
  • 1,609
  • 1
  • 13
  • 24
  • 3
    It's not clear what you find surprising. There's a simple boolean member in `unique_lock` that `owns_lock()` returns, and that gets moved by move constructor in a predictable and documented fashion. `owns_lock()` doesn't touch the underlying mutex. Having said that, your program exhibits undefined behavor: when `unique_lock` is destroyed on the worker thread, it calls `g_mutex.unlock()`, but the worker thread doesn't hold a lock on `g_mutex` (which is a pre-requisite for `unlock()` ). – Igor Tandetnik Jul 18 '16 at 14:09
  • 1
    @IgorTandetnik Thanks. So it's not possible to move ownership of a `recursive_mutex` between threads? What if the mutex wasn't recursive?Would moving the `unique_lock` really move ownership to the owner thread? – Mr. Anderson Jul 18 '16 at 14:12
  • 4
    Moving `unique_lock` between threads does absolutely nothing good for you. Realize that `unique_lock` is nothing more than a `mutex*` pointer and a `bool owns` flag - there's no black magic. The move constructor simply moves over that pointer and that boolean. Calling `my_mutex.unlock()` on a thread different from the one that called `my_mutex.lock()` exhibits undefined behavior, whether done explicitly or indirectly by tricking `unique_lock` into doing it. This is true of all mutex flavors. – Igor Tandetnik Jul 18 '16 at 14:15
  • @IgorTandetnik I think you've answered the question. Why not submit your combined comments as an answer? – jonspaceharper Jul 18 '16 at 15:52

1 Answers1

9

You appear to ascribe some magic properties to unique_lock. It doesn't have any, it's a very simple class. It has two data members, Mutex* pm and bool owns (member names shown for exposition only). lock() is simply pm->lock(); owns = true;, and unlock does pm->unlock(); owns = false;. The destructor is if (owns) unlock();. Move constructor copies over the two members, and sets them in the original to nullptr and false, correspondingly. owns_lock() returns the value of owns member.

All the thread-synchronizing magic is in the mutex itself, and its lock() and unlock() methods. unique_lock is merely a thin wrapper around it.

Now, the thread that calls mutex.unlock() must, as a prerequisite, hold the mutex (meaning, that thread has previously called lock() on it), or else the program exhibits undefined behavior. This is true whether you call unlock explicitly, or trick some helper like unique_lock into calling it for you.

In light of all this, moving a unique_lock instance over to another thread is merely a recipe for triggering undefined behavior shortly thereafter; there is no upside.

Igor Tandetnik
  • 50,461
  • 4
  • 56
  • 85