Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rationale to keep std::mutex lock/unlock public

My question is simple. In C++11 we have std::mutex and std::lock_guard and std::unique_lock.

The usual way to use these classes is to lock the std::mutex through any of the locks. This prevents leaking mutexes due to exception throwing:

{
   std::lock_guard<std::mutex> l(some_mutex);
   //Cannot leak mutex.
}

Why std::mutex::lock and std::mutex::unlock are public? That calls for incorrect usage:

{ 
     some_mutex.lock();
     //Mutex leaked due to exception.
     some_mutex.unlock();
}

Wouldn't be safer to make std::lock_guard and std::unique_lock a friend of std::mutex and make std::mutex lock operations private? This would prevent unsafe usage.

The only reason I can guess for this design is that if I have my own lock, it couldn't be used with std::mutex because I wouldn't be able to make my own lock friend of std::mutex. Is this the main reason behind making these member functions public?

like image 382
Germán Diago Avatar asked Nov 04 '13 03:11

Germán Diago


People also ask

What is the benefit of using STD unique lock between instances?

The benefit to using std::unique_lock<> comes from two things: you can transfer ownership of the lock between instances, and. the std::unique_lock<> object does not have to own the lock on the mutex it is associated with.

What happens when mutex is locked?

Mutexes are used to protect shared resources. If the mutex is already locked by another thread, the thread waits for the mutex to become available. The thread that has locked a mutex becomes its current owner and remains the owner until the same thread has unlocked it.

Are mutex locks fair?

One advantage of OS mutexes is that they guarantee fairness: All threads waiting for a lock form a queue, and, when the lock is released, the thread at the head of the queue acquires it. It's 100% deterministic.

Are mutex locks slow?

Secondly, the std::mutex is implemented in way that it spin locks for a bit before actually doing system calls when it fails to immediately obtain the lock on a mutex (which no doubt will be extremely slow).


2 Answers

The rationale for this design decision is documented in N2406:

Unlike boost, the mutexes have public member functions for lock(), unlock(), etc. This is necessary to support one of the primary goals: User defined mutexes can be used with standard defined locks. If there were no interface for the user defined mutex to implement, there would be no way for a standard defined lock to communicate with the user defined mutex.

At the time this was written, boost::mutex could only be locked and unlocked with boost::scoped_lock.

So you can now write my::mutex, and as long as you supply members lock() and unlock(), your my::mutex is just as much a first class citizen as std::mutex. Your clients can use std::unique_lock<my::mutex> just as easily as they can use std::unique_lock<std::mutex>, even though std::unique_lock has no clue what my::mutex is.

A motivating real-life example of my::mutex is the proposed std::shared_mutex now in the C++1y (we hope y == 4) draft standard. std::shared_mutex has members lock() and unlock() for the exclusive-mode locking and unlocking. And it interacts with std::unique_lock when the client uses std::unique_lock<std::shared_mutex> just as expected.

And just in case you might believe shared_mutex might be the only other motivating mutex that could make use of this generic interface, here's another real-world example: :-)

template <class L0, class L1>
void
lock(L0& l0, L1& l1)
{
    while (true)
    {
        {
            unique_lock<L0> u0(l0);
            if (l1.try_lock())
            {
                u0.release();
                break;
            }
        }
        this_thread::yield();
        {
            unique_lock<L1> u1(l1);
            if (l0.try_lock())
            {
                u1.release();
                break;
            }
        }
        this_thread::yield();
    }
}

This is the basic code for how to lock (in an exception safe manner) two BasicLockables at once, without danger of deadlock. And despite many critiques to the contrary, this code is extraordinarily efficient.

Note the line in the code above:

            unique_lock<L0> u0(l0);

This algorithm has no clue what type L0 is. But as long as it supports lock() and unlock() (ok and try_lock() too), everything is cool. L0 might even be another instantiation of unique_lock, perhaps unique_lock<my::mutex> or even unique_lock<std::shared_mutex>, or perhaps even std::shared_lock<std::shared_mutex>.

It all just works. And all because there is no intimate relationship between std::unique_lock and std::mutex aside from the agreed upon public interface of lock() and unlock().

like image 59
Howard Hinnant Avatar answered Oct 05 '22 23:10

Howard Hinnant


As Jerry Coffin pointed out, we can only speculate without an actual committee member chiming in, but my guess would be that they designed the interfaces with flexibility and the ability to extend them in your own way in mind. Instead of making every potential class a friend of std::mutex, exposing the interface to do the needed operations allows for someone (e.g. boost contributors) to come along and find a new and better way to do something with it. If the interface was hidden, that would not be possible.

like image 27
Zac Howland Avatar answered Oct 06 '22 01:10

Zac Howland