Howard Hinnant
2011-07-16

Shared locking in C++

Contents

Introduction

This propsal adds functionality to allow clients to easily code the well-known multiple-readers / single-writer locking pattern.

This proposal adds:

  1. Seven constructors to unique_lock<Mutex>.

    These constructors serve as explicit try and timed conversions from shared_lock and upgrade_lock.
  2. A new header: <shared_mutex>, containing:

    1. class shared_mutex;
    2. class upgrade_mutex;
    3. template <class Mutex> class shared_lock;
    4. template <class Mutex> class upgrade_lock;

This proposal is essentially the same proposal as N2094 which was designed 5 years ago. The point of bringing this up is that our current mutexes and locks were designed with these proposed mutexes and locks all as one coherent design. The shared and upgrade mutexes and locks form a hierarchy of concepts based on what we now call TimedLockable. Thus this proposal is not just a pure addition to the standard library. It is the other half of the original design, including the interactions between these two halves.

The second half of this design (this proposal) was excluded from C++11 based on non-technical reasons: We needed to limit the scope of C++0x in 2007 in hopes of shipping a new standard by 2009. This has become known as the Kona compromise. It is now time to seriously consider the oft-requested functionality of "reader/writer locks".

N2406 includes extensive rationale for shared and upgrade locking, as well as a high quality implementation made portable by our now standard std::mutex and std::condition_variable.

Overview for shared locking

First a brief review: Here is the synopsis of our current std::timed_mutex:

class timed_mutex
{
public:
     timed_mutex();
     ~timed_mutex();

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

    void lock();
    bool try_lock();
    template <class Rep, class Period>
        bool try_lock_for(const chrono::duration<Rep, Period>& rel_time);
    template <class Clock, class Duration>
        bool try_lock_until(const chrono::time_point<Clock, Duration>& abs_time);
    void unlock();
};

A shared_mutex has all of the syntax and functionality of a timed_mutex, and adds the following members:

void lock_shared();
bool try_lock_shared();
template <class Rep, class Period>
    bool
    try_lock_shared_for(const chrono::duration<Rep, Period>& rel_time);
template <class Clock, class Duration>
    bool
    try_lock_shared_until(const chrono::time_point<Clock, Duration>& abs_time);
void unlock_shared();

Thus a shared_mutex can be locked in one of two ownership modes:

  1. Exclusive, using lock(), or
  2. Shared, using lock_shared().

One uses the now familiar API of timed_mutex to lock a shared_mutex in exclusive mode (lock(), try_lock(), try_lock_for(), unlock(), etc.). Not only does this reuse help in learning and teaching shared_mutex, it is absolutely critical for generic code. One can have generic code lock a mutex in exclusive mode, and that code need not know or care whether it is locking a mutex or (exclusively) locking a shared_mutex. An immediate example of such generic code is lock_guard<shared_mutex>. Indeed, the recommended pattern for exclusively locking a shared_mutex in a scope is:

shared_mutex mut;
...
void foo()
{
     lock_guard<shared_mutex> _(mut);
     // mut lock'd here
     // ...
}    // mut.unlock() called here

Everything just works with our existing library. It was all designed together.

Now obviously shared_mutex has this extra lock ownership mode called shared and this new mode is shared_mutex's reason for existing. Multiple threads can can lock a shared_mutex in shared ownership mode at the same time (using e.g. m.lock_shared()). This is typically used when several threads have the job of reading (but not modifying) data, but should not read while another thread has the job of writing to the data. In such a pattern, greater concurrency can be achieved (higher performance) than with serialized access to the data for all threads.

The recommended pattern for locking a shared_mutex in shared ownership mode is not to call m.lock_shared() directly, but to use shared_lock<shared_mutex>:

shared_mutex mut;
...
void foo()
{
     shared_lock<shared_mutex> _(mut);
     // mut lock_shared'd here
     // ...
}    // mut.unlock_shared() called here

Furthermore, shared_lock<shared_mutex> is a Lockable type. This means that more generic code that we already have in the standard works with shared_lock<shared_mutex>. For example consider writing a copy assignment operator for a type protected by a shared_mutex. One might want exclusively lock the lhs, while share-locking the rhs. Here is the wrong way to do it:

class A
{
    mutable shared_mutex mut_;
    // more data...
public:
    A& operator=(const A& a)
    {
        unique_lock<shared_mutex> lhs(mut_);
        shared_lock<shared_mutex> rhs(a.mut_);  // Wrong!  Deadlock!
        // Assign data ...
        return *this;
    }
}

The above code can deadlock when one thread assigns a1 = a2 while another assigns a2 = a1. The two threads will lock a1.mut_ and a2.mut_ in opposite orders. And it doesn't matter that one of them is being locked exclusively and one in shared mode. There is still a deadlock here.

But use of our existing generic locking algorithm std::lock (to avoid deadlock) already works for both unique_lock<shared_mutex> and shared_lock<shared_mutex> because they are both Locakable types:

class A
{
    mutable shared_mutex mut_;
    // more data...
public:
    A& operator=(const A& a)
    {
        unique_lock<shared_mutex> lhs(mut_, defer_lock);
        shared_lock<shared_mutex> rhs(a.mut_, defer_lock);
        lock(lhs, rhs);  // mut_.lock() and a.mut_.lock_shared()
        // Assign data ...
        return *this;
    }   // a.mut_.unlock_shared() and mut_.unlock()
}

Everything just works with our existing library. It was all designed together.

Have you ever needed to wait on a condition variable with a mutex locked in shared mode? No? If you do much low-level concurrent programming, you will eventually need to. A shared ownership lock is just another lock, but allows more concurrency (higher performance). Surely it is not unlikely that a "reader thread" will need to sleep until it receives a signal that there is fresh data to be read. With the proposed shared_mutex and our existing condition_variable_any this is practically effortless:

shared_mutex mut;
condition_variable_any cv;
...
void foo()
{
     shared_lock<shared_mutex> sl(mut);
     // mut share locked here
     // ...
     while (not_ready_to_proceed())
         cv.wait(sl);  // mut unlocked while waiting
     // mut share locked here
     // ...
}    // mut.unlock_shared()

In the above example if you need to wait on the shared_mutex while it is exclusively locked instead of share-locked, no problem. Just use unique_lock<shared_mutex> instead.

Everything just works with our existing library. It was all designed together.

With the addition of shared_mutex and shared_lock combined with our existing unique_lock, std::lock and condition_variable_any, the client has the ability to program at an abstraction level that is an order of magnitude higher than the pthreads API (albeit still far below where many of you want to be). But this is an important part of the mid-level foundation needed for building those higher level abstractions. It builds upon what we have. And enables yet higher level abstractions to be efficiently built upon this mid-level layer.

You do not want to have to figure out how to wait on a pthread_rwlock_t locked with pthread_rwlock_rdlock (or with pthread_rwlock_wrlock for that matter), using a pthread_cond_t!!! It is not directly supported. But all of this functionality has been portably implemented on top of our C++11 std::lib, which in turn is implementable on top of both pthreads and Windows.

Spurious failures (or lack thereof)

Unlike our existing mutexes, these mutexes do not have try-locking functions that can fail spuriously. The reason is that having multiple locking states introduces the possibility of extremely motivating algorithms based on try-locking logic that require the lack of spurious failures. For example if you call m.try_lock_shared() and it returns false, being able to reason that another thread must have that mutex exclusively locked can be very powerful. E.g. now you know that the data is being updated by another thread.

The next section includes very reasonable example code that requires a try operation with no spurious failures.

Overview for upgrade locking

It is not uncommon for me to see a question along the lines of:

I have a mutex locked in shared (or read) mode and I need to atomically convert it to exclusive mode, without relinquishing the shared ownership. If I relinquish the shared ownership, then when I gain exclusive ownership, I can no longer depend on the expensive read operation I did under shared mode. How can I achieve this?

Straight-forward answer: You can't.

Elaborations I see too often: You don't want to anyway. There must be something wrong with your design.

I see this desire expressed often enough by coders just trying to get their job done that I know the elaborations are incorrect. There is a technical solution to this problem. But the solution can't be properly achieved without a holistic design with the exclusive and shared ownership modes of mutexes. That means it is our job to solve this problem, because it isn't practical for the coder to solve it in his domain. The solution involves adding constructors unique_lock, which the client obviously can not do.

The solution for the std::lib is relatively easy, quite economical, integrates well with mutex and shared_mutex, and was designed at the same time (5 years ago) as mutex and shared_mutex as one cohesive library.

The reason you can't upgrade a shared_mutex locked in shared ownership mode to exclusive ownership is because of deadlock. What do you do if two threads holding shared ownership on the same shared_mutex both request an atomic upgrade to exclusive ownership? You can only say yes to one of those threads. You can't guarantee any thread that they will be the chosen one to upgrade.

The answer to this dilemma is deceptively simple: Let the programmer decide which one of the threads with shared ownership will be the "privileged" one which is able to atomically upgrade to exclusive ownership. The mechanism for doing this is to add a third ownership mode: upgrade ownership.

A mutex locked in upgrade ownership mode can share ownership with other threads needing shared ownership. However it can not share with other threads holding upgrade ownership. And naturally it can't share with other threads holding exclusive ownership. So when a bunch of threads sharing ownership of a mutex exist (but only one of those threads actually has upgrade ownership — enforced by upgrade_mutex) then the thread with upgrade ownership can request a conversion to exclusive ownership and all it has to do is block until all the threads with shared ownership relinquish their shared ownership. At this point the thread with upgrade ownership is the only thread referencing the mutex and can atomically convert to exclusive ownership.

At this point it is good to review:

A mutex is capable of two locking states:

  1. Exclusively locked.
  2. Unlocked.

A shared_mutex is capable of three locking states:

  1. Exclusively locked.
  2. Share locked.
  3. Unlocked.

An upgrade_mutex is capable of four locking states:

  1. Exclusively locked.
  2. Upgrade locked.
  3. Share locked.
  4. Unlocked.

An Upgrade lock is a locked state that can share ownership with share locked states, but not with upgrade or exclusive lock states, and has the privilege of atomically converting from upgrade ownership to exclusive ownership.

An upgrade_mutex has all of the API and functionality of a shared_mutex (and thus also of a timed_mutex) and adds API for locking (and timed/try-locking) in upgrade ownership mode. And upgrade_mutex also adds API for converting among the three lock ownership modes:

  1. Exclusive <-> Upgrade
  2. Exclusive <-> Shared
  3. Upgrade <-> Shared

One can put these three modes in a graph like so:

exclusive
  |   \
  |    \
  |    upgrade
  |    /
  |   /
shared

Conversions traveling down the graph are always non-blocking.

Conversions traveling up the graph are always try or timed, except that there is also a non-try, non-timed, but blocking conversion from upgrade to exclusive. Only upgrade_mutex contains this conversion functionality among the three ownership locking modes. shared_mutex and mutex purposefully lack this conversion functionality. If you do not want your application to have the possibility of a locking mode conversion, then it can not do so if your application makes no use of upgrade_mutex.

With three locking modes, and conversions among the three locking modes, the API for upgrade_mutex is fairly large. The good news however is that programmers can easily make use of upgrade_mutex without having to deal with that large API directly. Instead one uses the three lock types: unique_lock<upgrade_mutex>, shared_lock<upgrade_mutex> and upgrade_lock<upgrade_mutex>.

If you want to lock an upgrade_mutex in exclusive mode you simply:

void test_exclusive()
{
    unique_lock<upgrade_mutex> lk(mut);
    // mut exclusive locked here
    // ...
}   // mut.unlock()

To lock an upgrade_mutex in upgrade mode you simply:

void test_upgrade()
{
    upgrade_lock<upgrade_mutex> lk(mut);
    // mut upgrade locked here
    // ...
}   // mut.unlock_upgrade()

And to lock an upgrade_mutex in shared mode is just:

void test_upgrade()
{
    shared_lock<upgrade_mutex> lk(mut);
    // mut share locked here
    // ...
}   // mut.unlock_shared()

That is, if you already know how to lock and unlock a mutex and shared_mutex, then you also already know how to lock and unlock an upgrade_mutex. The template lock classes make the syntax homogeneous. Easy to teach and great for generic code.

The templated locks also make the syntax of ownership conversion of an upgrade_mutex homogeneous. One simply move-converts from one lock type to another, using try_to_lock, a duration, or a time_point with the conversion as appropriate:

upgrade_mutex mut;
// ...
void foo()
{
    shared_lock<upgrade_mutex> sl(mut);
    // mut share locked here
    // ...
    // Try to convert mut from shared to upgrade ownership for 5ms
    // It can succeed if no other thread has upgrade ownership,
    //   or is blocked on exclusive ownership.
    upgrade_lock<upgrade_mutex> ul(std::move(sl), chrono::milliseconds(5));
    if (ul.owns_lock())
    {
        // mut upgrade locked here
        // This thread will still share with other threads having shared ownership here.
        // ...
        // Convert mut from upgrade to exclusive, blocking as necessary
        unique_lock<upgrade_mutex> el(std::move(ul));
        // mut exclusive locked here
        // No other threads have shared, upgrade or exclusive ownership here.
        // ...
    }  // mut.unlock()
    else
    {
        // mut still share locked here
        // ...
    }
}  // if mut is share locked, then mut.unlock_shared()

If you take all of the comments out of the example above you are left with very little code, and what's left is efficient, thread safe, exception safe, and self documenting.

If you accidentally attempt an illegal conversion (e.g. a non-timed, non-try conversion from shared to exclusive) a compile time error results:

error: no matching constructor for initialization of 'unique_lock<upgrade_mutex>'
unique_lock<upgrade_mutex> el(std::move(sl));
                           ^  ~~~~~~~~~~~~~

And so the quite complex API of upgrade_mutex is completely handled by constructing and converting among the three lock types in a manner that is easy to teach, and is very applicable to generic locking code such as condition_variable_any and std::lock(lk1, lk2, lk3, ...)

Everything just works with our existing library. It was all designed together.

It is instructive to look at an example implementation of generic locking code that already exists in our C++11 std::library. This is part of the std::lock algorithm:

template <class L0, class L1>
void
lock(L0& lk0, L1& lk1)
{
    while (true)
    {
        {
            unique_lock<L0> ul(lk0);
            if (lk1.try_lock())
            {
                ul.release();
                break;
            }
        }
        this_thread::yield();
        {
            unique_lock<L1> ul(lk1);
            if (lk0.try_lock())
            {
                ul.release();
                break;
            }
        }
        this_thread::yield();
    }
}

The above code just works (thread safe, exception safe, efficient, etc.), even if L0 and/or L1 is a shared_lock<upgrade_mutex>, upgrade_lock<upgrade_mutex>, or a simple mutex. If you look closely, this means that forming a unique_lock<shared_lock<upgrade_mutex>> is not only ok, it is actually useful! This is so because all of these types adhere to the Lockable requirements. This is true, even though some types will be obtaining shared or upgrade ownership while others may be obtaining exclusive ownership.

Motivating Example

Below is one final motivating example for shared ownership conversion as proposed herein. I should also point out that there exists a boost implementation inspired by N2094. But that implementation made some serious adjustments to the design (for the worse in my opinion) and will not work for this example problem, as it is lacking the required functionality.

Consider the case of a database, say implemented with one of the standard associative containers (this code uses std::map<int, int> for a simple concrete example). The database is designed to be a read-only database, and accessible by many threads concurrently. And yet the designer wishes it to be constructed lazily: Only when a thread first requests data is the data actually computed and added to the database. And from then on, subsequent requests for that data are a simple lookup.

Thus the general algorithm for:

int get_data(int x) const;

is:

  1. Search the database for x.
  2. If x exists in the database, return the associated data y.
  3. Else x does not exist in the database. Compute the associated data y, and store the key and associated data in the database so that it can be more quickly retrieved by the next request.

And this all needs to work with multiple threads calling get_data(x) concurrently.

A brute force approach is to wrap the entire algorithm in a (exclusively locking) mutex. However if step 1 takes considerable time, it might be advantageous to let many threads execute steps 1 and 2 concurrently since these steps do not modify the database. We will proceed on this assumption for the purpose of demonstrating a motivating use case for upgrade_mutex.

Below is the code, with a more detailed explanation of the code following:

class Database
{
    // types
    typedef std::map<int, int>              map_type;
    typedef proposed::upgrade_mutex         mutex_t;
    typedef proposed::shared_lock<mutex_t>  shared_lock;
    typedef proposed::upgrade_lock<mutex_t> upgrade_lock;
    typedef      std::unique_lock<mutex_t>  unique_lock;

    // members
    mutable mutex_t mtx_;
    mutable std::condition_variable_any cv_;
    mutable map_type data_;

public:
    int get_data(int x) const
    {
        typename map_type::iterator i;
        // Obtain a shared lock.
        shared_lock sl(mtx_);
        while (true)
        {
            // Read the database
            i = data_.find(x);
            // Is the data in the database?
            if (i != data_.end())
                // Yes, the data is in the database, return it
                break;
            // No, it is not.
            // Try to obtain upgrade ownership
            upgrade_lock ul(std::move(sl), std::try_to_lock);
            if (ul.owns_lock())
            {
                // This is the one thread that was able to get upgrade
                //    ownership.
                // Compute the new value to go into the database.
                // Other threads can still share the database
                int y = compute(x);
                // Now wait until all other threads release shared ownership:
                unique_lock el(std::move(ul));
                // This thread now has exclusive ownership.
                // Create entry and then notify all sleeping threads that
                //   the entry exists.
                i = data_.insert(std::make_pair(x, y)).first;
                cv_.notify_all();
                // Return new entry
                break;
            }
            // Some other thread is trying to create an entry.
            //   So sleep until that work is done.
            cv_.wait(sl);
            //   And then read the database again
        }
        // There is now an entry in data_ corresponding to x
        return i->second;
    }
};

The first step is to get shared ownership so that we can search the database. shared_lock is used to lock the mutex in shared ownership mode. At this point the mutex type could be either shared_mutex or upgrade_mutex. However later in the algorithm we need to convert the ownership mode of the mutex without first unlocking it. Thus upgrade_mutex is the proper tool.

With mtx_ locked in shared ownership mode we can safely search the database. We can do this concurrently with other threads needing to search the database. But all threads trying to update the database are blocked during our search.

If we find the data in the database (the if condition evaluates to true) then we merely return the associated data. sl implicitly calls mtx_.unlock_shared() as we leave its scope.

However if we need to update the database, then we try to convert the shared ownership into upgrade ownership. It is crucial that if the try-conversion fails, that it not do so spuriously. Because if the try-conversion fails, this code assumes that some other thread is busy updating the database, and will notify us as we sleep when it is done. If there is no such thread to notify us, then we sleep forever (deadlock).

If we are successful in converting our shared ownership to upgrade ownership, this means that we are next in line to obtain exclusive ownership. However at this point we are still sharing ownership with other threads having locked in shared ownership mode (other threads can still search). We now go to the trouble to compute the associated data y. This might be a lengthy operation. But other threads continue to be able to search the database while we are busy.

Once we have computed the associated data, we are ready to update the database with it. To do this we must convert our upgrade ownership to exclusive ownership. This is a blocking operation that will not fail. This thread simply has to block until all other threads relinquish their shared ownership. None of these other threads will be successful in converting their shared ownership to upgrade (or exclusive) while we block. An implementation is strongly encouraged to block other threads requesting shared ownership while this thread is blocked waiting for exclusive ownership, lest the blocked thread be starved by readers.

Once we return from this blocking operation, we have exclusive ownership of the mutex, and no other thread has obtained exclusive ownership since we discovered that x is missing from the database. Thus we know the data is still missing. We can simply insert the data. And then notify all threads waiting on the condition variable that it is time to retry their operation.

As we return the associated data, el implicitly calls mtx_.unlock().

In the case that we fail to convert our ownership to upgrade, we simply wait on cv_, a std::condition_variable_any. As we wait on this condition variable, we implicitly relinquish our shared lock. And when we return from the wait, we implicitly obtain the shared lock back. Relinquishing the shared ownership while waiting is crucial as otherwise conversions from upgrade to exclusive could not happen while we wait. Note that std::condition_variable_any already has this behavior we require, even though it knows nothing about our new type: proposed::shared_lock<proposed::upgrade_mutex>. On returning, we simply restart our operation with a new search (without relinquishing our shared ownership). Another thread may have put our data into the database by now!

Note that an alternative design of try-converting from shared to exclusive, instead of from shared to upgrade as shown, would be susceptible to update (writer) starvation. This is because as long as there are multiple searchers (shared locks), none of the searchers can ever successfully try-convert to updaters. It is only by successfully registering yourself as the single thread having upgrade ownership and then blocking on a conversion from upgrade to exclusive, do you enable the implementation to start blocking new searchers from obtaining a shared lock so that you can eventually obtain the exclusive lock as existing searchers are cleared out.

Another alternative design would relinquish shared ownership and then block on an exclusive lock, when needing to update the database. This would likely not be as efficient for the case that the search is more expensive than a few tens of microseconds. Once the exclusive ownership is obtained, the algorithm would then have to redo the search to confirm that another thread had not added the key between the time this thread relinquished shared ownership, and the time it gained exclusive ownership. By retaining shared ownership while upgrading to exclusive ownership this algorithm avoids the need to redo the search.

The above example is a realistic and motivating example making use of shared ownership and conversions among the ownership modes. Whether or not it is a performance win will depend on how expensive the search and update operations are. The code is thread safe, exception safe, quite readable, and not prone to run time errors. If the author of the code accidentally attempts an invalid conversion, such as forgetting the "try" in the shared-to-upgrade conversion, a compile-time error results. This is an illegal conversion (without the "try") because it could otherwise result in deadlock.

Said differently, this design is eliminating conversion-related deadlocks at compile time!

Proposed Wording

Add a synopsis to 30.4. [thread.mutex] as follows:

Header <shared_mutex> synopsis

namespace std {

class shared_mutex;
class upgrade_mutex;
template <class Mutex> class shared_lock;
template <class Mutex>
  void swap(shared_lock<Mutex>& x, shared_lock<Mutex>& y) noexcept;
template <class Mutex> class upgrade_lock;
template <class Mutex>
  void swap(upgrade_lock<Mutex>& x, upgrade_lock<Mutex>& y) noexcept;

}  // std

Modify 30.4.1.2 [thread.mutex.requirements.mutex] as follows:

1 The mutex types are the standard library types std::mutex, std::recursive_mutex, std::timed_mutex, and std::recursive_timed_mutex, std::shared_mutex, and std::upgrade_mutex. They shall meet the requirements set out in this section. In this description, m denotes an object of a mutex type.

Modify 30.4.1.3 [thread.timedmutex.requirements] as follows:

1 The timed mutex types are the standard library types std::timed_mutex, and std::recursive_timed_mutex, std::shared_mutex, and std::upgrade_mutex. They shall meet the requirements set out below. In this description, m denotes an object of a mutex type, rel_time denotes an object of an instantiation of duration (20.11.5), and abs_time denotes an object of an instantiation of time_point (20.11.6).

Insert a new section: 30.4.1.4 Shared mutex types [thread.sharedmutex.requirements]

The shared mutex types are the standard library types std::shared_mutex and std::upgrade_mutex. They shall meet the requirements of timed mutex types ([thread.timedmutex.requirements]), and additionally shall meet the requirements set out below. In this description, m denotes an object of a mutex type, rel_type denotes an object of an instantiation of duration (20.11.5), and abs_time denotes an object of an instantiation of time_point (20.11.6).

In addition to the exclusive lock ownership mode specified in [thread.mutex.requirements.mutex], shared mutex types provide a shared lock ownership mode. Multiple execution agents can simultaneously hold a shared lock ownership of a shared mutex type. But no execution agent shall hold a shared lock while another execution agent holds an exclusive lock on the same shared mutex type, and vice-versa. The maximum number of execution agents which can share a shared lock on a single shared mutex type is unspecified, but shall be at least 10000. If more than the maximum number of execution agents attempt to obtain a shared lock, the excess execution agents shall block until the number of shared locks are reduced below the maximum amount by other execution agents releasing their shared lock.

Additionally shared mutex types can not fail spuriously for their try and timed operations, for either exclusive locking or shared locking modes.

The expression m.lock_shared() shall be well-formed and have the following semantics:

Requires: The calling thread has no ownership of the mutex.

Effects: Blocks the calling thread until shared ownership of the mutex can be obtained for the calling thread.

Postcondition: The calling thread has a shared lock on the mutex.

Return type: void.

Synchronization: Prior unlock_shared() operations on the same object shall synchronize with (1.10) this operation.

Throws: system_error when an exception is required (30.2.2).

Error conditions:

The expression m.unlock_shared() shall be well-formed and have the following semantics:

Requires: The calling thread shall hold a shared lock on the mutex.

Effects: Releases a shared lock on the mutex held by the calling thread.

Return type: void.

Synchronization: This operation synchronizes with (1.10) subsequent lock operations that obtain ownership on the same object.

Throws: Nothing.

The expression m.try_lock_shared() shall be well-formed and have the following semantics:

Requires: The calling thread has no ownership of the mutex.

Effects: Attempts to obtain shared ownership of the mutex for the calling thread without blocking. If shared ownership is not obtained, there is no effect and try_lock_shared() immediately returns.

Return type: bool.

Returns: true if the shared ownership lock was acquired, false otherwise.

Synchronization: If try_lock_shared() returns true, prior unlock_shared() operations on the same object synchronize with (1.10) this operation.

Throws: Nothing.

The expression m.try_lock_shared_for(rel_time) shall be well-formed and have the following semantics:

Requires: The calling thread has no ownership of the mutex.

Effects: If the tick period of rel_time is not exactly convertible to the native tick period, the duration shall be rounded up to the nearest native tick period. Attempts to obtain shared lock ownership for the calling thread within the relative timeout (30.2.4) specified by rel_time. If the time specified by rel_time is less than or equal to rel_time.zero(), the function attempts to obtain ownership without blocking (as if by calling try_lock_shared()>). The function shall return within the timeout specified by rel_time only if it has obtained shared ownership of the mutex object.

Return type: bool.

Returns: true if the shared lock was acquired, false otherwise.

Synchronization: If try_lock_shared_for() returns true, prior unlock_shared() operations on the same object synchronize with (1.10) this operation.

Throws: Nothing.

The expression m.try_lock_shared_until(abs_time) shall be well-formed and have the following semantics:

Requires: The calling thread has no ownership of the mutex.

Effects: The function attempts to obtain shared ownership of the mutex. If abs_time has already passed, the function attempts to obtain shared ownership without blocking (as if by calling try_lock_shared()). The function shall return before the absolute timeout (30.2.4) specified by abs_time only if it has obtained shared ownership of the mutex object.

Return type: bool.

Returns: true if the shared lock was acquired, false otherwise.

Synchronization: If try_lock_shared_until() returns true, prior unlock_shared() operations on the same object synchronize with (1.10) this operation.

Throws: Nothing.

Insert a new section: 30.4.1.4.1 Class shared_mutex [thread.sharedmutex.class]

namespace std {

class shared_mutex
{
public:

    shared_mutex();
    ~shared_mutex();

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

    // Exclusive ownership

    void lock();  // blocking
    bool try_lock();
    template <class Rep, class Period>
        bool try_lock_for(const chrono::duration<Rep, Period>& rel_time);
    template <class Clock, class Duration>
        bool
        try_lock_until(const chrono::time_point<Clock, Duration>& abs_time);
    void unlock();

    // Shared ownership

    void lock_shared();  // blocking
    bool try_lock_shared();
    template <class Rep, class Period>
        bool
        try_lock_shared_for(const chrono::duration<Rep, Period>& rel_time);
    template <class Clock, class Duration>
        bool
        try_lock_shared_until(
                      const chrono::time_point<Clock, Duration>& abs_time);
    void unlock_shared();
};

}  // std

The class shared_mutex provides a non-recursive mutex with shared ownership semantics.

The class shared_mutex shall satisfy all of the SharedMutex requirements ([thread.sharedmutex.requirements]). It shall be a standard-layout class (Clause 9).

The behavior of a program is undefined if:

Insert a new section: 30.4.1.5 Class upgrade_mutex [thread.upgrademutex.class]

In addition to the exclusive lock ownership mode specified in [thread.mutex.requirements.mutex], and the shared lock ownership specified in [thread.sharedmutex.requirements], upgrade_mutex provides a third ownership mode. A thread with an upgrade lock ownership does not share ownership with other threads holding exclusive or upgrade ownership modes. It does however share ownership with other threads holding shared ownership mode. This mutex type also has the functionality to atomically convert among exclusive, upgrade and shared ownership modes.

namespace std {

class upgrade_mutex
{
public:

    upgrade_mutex();
    ~upgrade_mutex();

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

    // Exclusive ownership

    void lock();  // blocking
    bool try_lock();
    template <class Rep, class Period>
        bool try_lock_for(const chrono::duration<Rep, Period>& rel_time);
    template <class Clock, class Duration>
        bool
        try_lock_until(
                      const chrono::time_point<Clock, Duration>& abs_time);
    void unlock();

    // Shared ownership

    void lock_shared();  // blocking
    bool try_lock_shared();
    template <class Rep, class Period>
        bool
        try_lock_shared_for(const chrono::duration<Rep, Period>& rel_time);
    template <class Clock, class Duration>
        bool
        try_lock_shared_until(
                      const chrono::time_point<Clock, Duration>& abs_time);
    void unlock_shared();

    // Upgrade ownership

    void lock_upgrade();  // blocking
    bool try_lock_upgrade();
    template <class Rep, class Period>
        bool
        try_lock_upgrade_for(
                            const chrono::duration<Rep, Period>& rel_time);
    template <class Clock, class Duration>
        bool
        try_lock_upgrade_until(
                      const chrono::time_point<Clock, Duration>& abs_time);
    void unlock_upgrade();

    // Shared <-> Exclusive

    bool try_unlock_shared_and_lock();
    template <class Rep, class Period>
        bool
        try_unlock_shared_and_lock_for(
                            const chrono::duration<Rep, Period>& rel_time);
    template <class Clock, class Duration>
        bool
        try_unlock_shared_and_lock_until(
                      const chrono::time_point<Clock, Duration>& abs_time);
    void unlock_and_lock_shared();

    // Shared <-> Upgrade

    bool try_unlock_shared_and_lock_upgrade();
    template <class Rep, class Period>
        bool
        try_unlock_shared_and_lock_upgrade_for(
                            const chrono::duration<Rep, Period>& rel_time);
    template <class Clock, class Duration>
        bool
        try_unlock_shared_and_lock_upgrade_until(
                      const chrono::time_point<Clock, Duration>& abs_time);
    void unlock_upgrade_and_lock_shared();

    // Upgrade <-> Exclusive

    void unlock_upgrade_and_lock();  // blocking
    bool try_unlock_upgrade_and_lock();
    template <class Rep, class Period>
        bool
        try_unlock_upgrade_and_lock_for(
                            const chrono::duration<Rep, Period>& rel_time);
    template <class Clock, class Duration>
        bool
        try_unlock_upgrade_and_lock_until(
                      const chrono::time_point<Clock, Duration>& abs_time);
    void unlock_and_lock_upgrade();
};

}  // std

upgrade_mutex shall meet the requirements of a shared mutex type ([thread.sharedmutex.requirements]) in addition to the requirements set out below.

upgrade_mutex can not fail spuriously for the try and timed operations for any locking any of the ownership modes, or conversions among those modes.

m.lock_upgrade()

Requires: The calling thread has no ownership of the mutex.

Effects: Blocks the calling thread until upgrade ownership of the mutex can be obtained for the calling thread.

Postcondition: The calling thread has an upgrade lock on the mutex.

Return type: void.

Synchronization: Prior unlock_upgrade() operations on the same object shall synchronize with (1.10) this operation.

Throws: system_error when an exception is required (30.2.2).

Error conditions:

m.unlock_upgrade()

Requires: The calling thread shall hold an upgrade lock on the mutex.

Effects: Releases an upgrade lock on the mutex held by the calling thread.

Return type: void.

Synchronization: This operation synchronizes with (1.10) subsequent lock operations that obtain ownership on the same object.

Throws: Nothing.

m.try_lock_upgrade()

Requires: The calling thread has no ownership of the mutex.

Effects: Attempts to obtain upgrade ownership of the mutex for the calling thread without blocking. If upgrade ownership is not obtained, there is no effect and try_lock_upgrade() immediately returns.

Return type: bool.

Returns: true if the upgrade ownership lock was acquired, false otherwise.

Synchronization: If try_lock_upgrade() returns true, prior unlock_upgrade() operations on the same object synchronize with (1.10) this operation.

Throws: Nothing.

m.try_lock_upgrade_for(rel_time)

Requires: The calling thread has no ownership of the mutex.

Effects: If the tick period of rel_time is not exactly convertible to the native tick period, the duration shall be rounded up to the nearest native tick period. Attempts to obtain upgrade lock ownership for the calling thread within the relative timeout (30.2.4) specified by rel_time. If the time specified by rel_time is less than or equal to rel_time.zero(), the function attempts to obtain ownership without blocking (as if by calling try_lock_upgrade()>). The function shall return within the timeout specified by rel_time only if it has obtained upgrade ownership of the mutex object.

Return type: bool.

Returns: true if the upgrade lock was acquired, false otherwise.

Synchronization: If try_lock_upgrade_for() returns true, prior unlock_upgrade() operations on the same object synchronize with (1.10) this operation.

Throws: Nothing.

m.try_lock_upgrade_until(abs_time)

Requires: The calling thread has no ownership of the mutex.

Effects: The function attempts to obtain upgrade ownership of the mutex. If abs_time has already passed, the function attempts to obtain upgrade ownership without blocking (as if by calling try_lock_upgrade()). The function shall return before the absolute timeout (30.2.4) specified by abs_time only if it has obtained upgrade ownership of the mutex object.

Return type: bool.

Returns: true if the upgrade lock was acquired, false otherwise.

Synchronization: If try_lock_upgrade_until() returns true, prior unlock_upgrade() operations on the same object synchronize with (1.10) this operation.

Throws: Nothing.

m.try_unlock_shared_and_lock()

Requires: The calling thread shall hold a shared lock on m.

Effects: The function attempts to atomically convert the ownership from shared to exclusive for the calling thread without blocking. For this conversion to be successful, this thread shall be the only thread holding any ownership of the lock. If the conversion is not successful, the shared ownership of m is retained.

Return type: bool.

Returns: true if the exclusive lock was acquired, false otherwise.

Synchronization: If try_unlock_shared_and_lock() returns true, prior unlock() and subsequent lock operations on the same object synchronize with (1.10) this operation.

Throws: Nothing.

m.try_unlock_shared_and_lock_for(rel_time)

Requires: The calling thread shall hold a shared lock on m.

Effects: If the tick period of rel_time is not exactly convertible to the native tick period, the duration shall be rounded up to the nearest native tick period. The function attempts to atomically convert the ownership from shared to exclusive for the calling thread within the relative timeout (30.2.4) specified by rel_time. If the time specified by rel_time is less than or equal to rel_time.zero(), the function attempts to obtain exclusive ownership without blocking (as if by calling try_unlock_shared_and_lock()>). The function shall return within the timeout specified by rel_time only if it has obtained exclusive ownership of the mutex object. For this conversion to be successful, this thread shall be the only thread holding any ownership of the lock at the moment of conversion. If the conversion is not successful, the shared ownership of m is retained.

Return type: bool.

Returns: true if the exclusive lock was acquired, false otherwise.

Synchronization: If try_unlock_shared_and_lock_for() returns true, prior unlock() and subsequent lock operations on the same object synchronize with (1.10) this operation.

Throws: Nothing.

m.try_unlock_shared_and_lock_until(abs_time)

Requires: The calling thread shall hold a shared lock on m.

Effects: The function attempts to atomically convert the ownership from shared to exclusive for the calling thread within the absolute timeout (30.2.4) specified by abs_time. If abs_time has already passed, the function attempts to obtain exclusive ownership without blocking (as if by calling try_unlock_shared_and_lock()>). The function shall return before the absolute timeout (30.2.4) specified by abs_time only if it has obtained exclusive ownership of the mutex object. For this conversion to be successful, this thread shall be the only thread holding any ownership of the lock at the moment of conversion. If the conversion is not successful, the shared ownership of m is retained.

Return type: bool.

Returns: true if the exclusive lock was acquired, false otherwise.

Synchronization: If try_unlock_shared_and_lock_until() returns true, prior unlock() and subsequent lock operations on the same object synchronize with (1.10) this operation.

Throws: Nothing.

m.unlock_and_lock_shared()

Requires: The calling thread shall hold an exclusive lock on m.

Effects: The function atomically converts the ownership from exclusive to shared for the calling thread.

Return type: void.

Synchronization: This operation synchronizes with (1.10) subsequent lock operations that obtain ownership of the same object.

Throws: Nothing.

m.try_unlock_shared_and_lock_upgrade()

Requires: The calling thread shall hold a shared lock on m.

Effects: The function attempts to atomically convert the ownership from shared to upgrade for the calling thread without blocking. For this conversion to be successful, there shall be no thread holding upgrade ownership of this object. If the conversion is not successful, the shared ownership of m is retained.

Return type: bool.

Returns: true if the exclusive lock was acquired, false otherwise.

Synchronization: If try_unlock_shared_and_lock_upgrade() returns true, prior unlock_upgrade() and subsequent lock operations on the same object synchronize with (1.10) this operation.

Throws: Nothing.

m.try_unlock_shared_and_lock_upgrade_for(rel_time)

Requires: The calling thread shall hold a shared lock on m.

Effects: If the tick period of rel_time is not exactly convertible to the native tick period, the duration shall be rounded up to the nearest native tick period. The function attempts to atomically convert the ownership from shared to upgrade for the calling thread within the relative timeout (30.2.4) specified by rel_time. If the time specified by rel_time is less than or equal to rel_time.zero(), the function attempts to obtain upgrade ownership without blocking (as if by calling try_unlock_shared_and_lock_upgrade()>). The function shall return within the timeout specified by rel_time only if it has obtained exclusive ownership of the mutex object. For this conversion to be successful, there shall be no thread holding upgrade ownership of this object at the moment of conversion. If the conversion is not successful, the shared ownership of m is retained.

Return type: bool.

Returns: true if the upgrade lock was acquired, false otherwise.

Synchronization: If try_unlock_shared_and_lock_upgrade_for() returns true, prior unlock_upgrade() and subsequent lock operations on the same object synchronize with (1.10) this operation.

Throws: Nothing.

m.try_unlock_shared_and_lock_upgrade_until(abs_time)

Requires: The calling thread shall hold a shared lock on m.

Effects: The function attempts to atomically convert the ownership from shared to upgrade for the calling thread within the absolute timeout (30.2.4) specified by abs_time. If abs_time has already passed, the function attempts to obtain upgrade ownership without blocking (as if by calling try_unlock_shared_and_lock_upgrade()>). The function shall return before the absolute timeout (30.2.4) specified by abs_time only if it has obtained upgrade ownership of the mutex object. For this conversion to be successful, there shall be no thread holding upgrade ownership of this object at the moment of conversion. If the conversion is not successful, the shared ownership of m is retained.

Return type: bool.

Returns: true if the upgrade lock was acquired, false otherwise.

Synchronization: If try_unlock_shared_and_lock_for() returns true, prior unlock() and subsequent lock operations on the same object synchronize with (1.10) this operation.

Throws: Nothing.

m.unlock_and_lock_upgrade()

Requires: The calling thread shall hold an exclusive lock on m.

Effects: The function atomically converts the ownership from exclusive to upgrade for the calling thread.

Return type: void.

Synchronization: This operation synchronizes with (1.10) subsequent lock operations that obtain ownership of the same object.

Throws: Nothing.

m.unlock_upgrade_and_lock()

Requires: The calling thread shall hold an upgrade lock on m.

Effects: Blocks the calling thread until the ownership can be converted from upgrade to exclusive ownership for this mutex. Until exclusive ownership is obtained, the calling thread maintains upgrade ownership of the mutex. Thus no other thread can obtain exclusive ownership of the mutex after this call is made, until this thread relinquishes all ownership of this mutex object. All other threads holding shared ownership of this mutex object shall call unlock_shared() prior to this function returning.

Return type: void.

Synchronization: This operation synchronizes with (1.10) prior unlock_shared() and subsequent lock operations that obtain ownership of the same object.

Throws: Nothing.

[Note: If a thread is blocked on this call, an implementation is required to block other threads calling lock() on the same mutex object, and is strongly encouraged to block other threads calling lock_shared() on the same mutex object. — end note]

m.try_unlock_upgrade_and_lock()

Requires: The calling thread shall hold an upgrade lock on m.

Effects: The function attempts to atomically convert the ownership from upgrade to exclusive for the calling thread without blocking. For this conversion to be successful, this thread shall be the only thread holding any ownership of the lock. If the conversion is not successful, the upgrade ownership of m is retained.

Return type: bool.

Returns: true if the exclusive lock was acquired, false otherwise.

Synchronization: If try_unlock_upgrade_and_lock() returns true, prior unlock() and subsequent lock operations on the same object synchronize with (1.10) this operation.

Throws: Nothing.

m.try_unlock_upgrade_and_lock_for(rel_time)

Requires: The calling thread shall hold an upgrade lock on m.

Effects: If the tick period of rel_time is not exactly convertible to the native tick period, the duration shall be rounded up to the nearest native tick period. The function attempts to atomically convert the ownership from upgrade to exclusive for the calling thread within the relative timeout (30.2.4) specified by rel_time. If the time specified by rel_time is less than or equal to rel_time.zero(), the function attempts to obtain exclusive ownership without blocking (as if by calling try_unlock_upgrade_and_lock()>). The function shall return within the timeout specified by rel_time only if it has obtained exclusive ownership of the mutex object. For this conversion to be successful, this thread shall be the only thread holding any ownership of the lock at the moment of conversion. If the conversion is not successful, the upgrade ownership of m is retained.

Return type: bool.

Returns: true if the exclusive lock was acquired, false otherwise.

Synchronization: If try_unlock_upgrade_and_lock_for() returns true, prior unlock() and subsequent lock operations on the same object synchronize with (1.10) this operation.

Throws: Nothing.

m.try_unlock_upgrade_and_lock_until(abs_time)

Requires: The calling thread shall hold an upgrade lock on m.

Effects: The function attempts to atomically convert the ownership from upgrade to exclusive for the calling thread within the absolute timeout (30.2.4) specified by abs_time. If abs_time has already passed, the function attempts to obtain exclusive ownership without blocking (as if by calling try_unlock_upgrade_and_lock()>). The function shall return before the absolute timeout (30.2.4) specified by abs_time only if it has obtained exclusive ownership of the mutex object. For this conversion to be successful, this thread shall be the only thread holding any ownership of the lock at the moment of conversion. If the conversion is not successful, the upgrade ownership of m is retained.

Return type: bool.

Returns: true if the exclusive lock was acquired, false otherwise.

Synchronization: If try_unlock_upgrade_and_lock_until() returns true, prior unlock() and subsequent lock operations on the same object synchronize with (1.10) this operation.

Throws: Nothing.

m.unlock_and_lock_upgrade()

Requires: The calling thread shall hold an exclusive lock on m.

Effects: The function atomically converts the ownership from exclusive to upgrade for the calling thread.

Return type: void.

Synchronization: This operation synchronizes with (1.10) subsequent lock operations that obtain ownership of the same object.

Throws: Nothing.

Add to the synopsis of unique_lock in [thread.lock.unique]:

 namespace std {
    template 
    class unique_lock {
    public:
    ...
        unique_lock(shared_lock<mutex_type>&&, try_to_lock_t);
        template <class Clock, class Duration>
            unique_lock(shared_lock<mutex_type>&&,
                        const chrono::time_point<Clock, Duration>&);
        template <class Rep, class Period>
            unique_lock(shared_lock<mutex_type>&&,
                        const chrono::duration<Rep, Period>&);
    
        explicit unique_lock(upgrade_lock<mutex_type>&&);
        unique_lock(upgrade_lock<mutex_type>&&, try_to_lock_t);
        template <class Clock, class Duration>
            unique_lock(upgrade_lock<mutex_type>&&,
                        const chrono::time_point<Clock, Duration>&);
        template <class Rep, class Period>
            unique_lock(upgrade_lock<mutex_type>&&,
                        const chrono::duration<Rep, Period>&);
    ...
    };

Add to [thread.lock.unique.cons]:

unique_lock(shared_lock<mutex_type>&& sl, try_to_lock_t);

Requires: The supplied Mutex type shall implement try_unlock_shared_and_lock() ([thread.upgrademutex.class]).

Effects: Constructs an object of type unique_lock, initializing pm with nullptr and owns with false. If sl.owns_lock() returns false, sets pm to the return value of sl.release(). Else sl.owns_lock() returns true, and in this case if sl.mutex()->try_unlock_shared_and_lock() returns true, sets pm to the value returned by sl.release() and sets owns to true. [Note: If sl.owns_lock() returns true and sl.mutex()->try_unlock_shared_and_lock() returns false, sl is not modified. — end note]

Throws: Nothing.

template <class Clock, class Duration>
    unique_lock(shared_lock<mutex_type>&& sl,
                const chrono::time_point<Clock, Duration>& abs_time)

Requires: The supplied Mutex type shall implement try_unlock_shared_and_lock_until() ([thread.upgrademutex.class]).

Effects: Constructs an object of type unique_lock, initializing pm with nullptr and owns with false. If sl.owns_lock() returns false, sets pm to the return value of sl.release(). Else sl.owns_lock() returns true, and in this case if sl.mutex()->try_unlock_shared_and_lock_until(abs_time) returns true, sets pm to the value returned by sl.release() and sets owns to true. [Note: If sl.owns_lock() returns true and sl.mutex()->try_unlock_shared_and_lock_until(abs_time) returns false, sl is not modified. — end note]

Throws: Nothing.

template <class Rep, class Period>
    unique_lock(shared_lock<mutex_type>&& sl,
                const chrono::duration<Rep, Period>& rel_time);

Requires: The supplied Mutex type shall implement try_unlock_shared_and_lock_for() ([thread.upgrademutex.class]).

Effects: Constructs an object of type unique_lock, initializing pm with nullptr and owns with false. If sl.owns_lock() returns false, sets pm to the return value of sl.release(). Else sl.owns_lock() returns true, and in this case if sl.mutex()->try_unlock_shared_and_lock_for(rel_time) returns true, sets pm to the value returned by sl.release() and sets owns to true. [Note: If sl.owns_lock() returns true and sl.mutex()->try_unlock_shared_and_lock_for(rel_time) returns false, sl is not modified. — end note]

Throws: Nothing.

explicit unique_lock(upgrade_lock<mutex_type>&& ul);

Requires: The supplied Mutex type shall implement unlock_upgrade_and_lock() ([thread.upgrademutex.class]).

Effects: Constructs an object of type unique_lock, initializing pm with ul.release() and owns with ul.owns_lock(). If owns is true, calls pm->unlock_upgrade_and_lock(). [Note: This is a blocking call. — end note]

Throws: Nothing.

unique_lock(upgrade_lock<mutex_type>&& ul, try_to_lock_t);

Requires: The supplied Mutex type shall implement try_unlock_upgrade_and_lock() ([thread.upgrademutex.class]).

Effects: Constructs an object of type unique_lock, initializing pm with nullptr and owns with false. If ul.owns_lock() returns false, sets pm to the return value of ul.release(). Else ul.owns_lock() returns true, and in this case if ul.mutex()->try_unlock_upgrade_and_lock() returns true, sets pm to the value returned by ul.release() and sets owns to true. [Note: If ul.owns_lock() returns true and ul.mutex()->try_unlock_upgrade_and_lock() returns false, ul is not modified. — end note]

Throws: Nothing.

template <class Clock, class Duration>
    unique_lock(upgrade_lock<mutex_type>&& ul,
                const chrono::time_point<Clock, Duration>& abs_time)

Requires: The supplied Mutex type shall implement try_unlock_upgrade_and_lock_until() ([thread.upgrademutex.class]).

Effects: Constructs an object of type unique_lock, initializing pm with nullptr and owns with false. If ul.owns_lock() returns false, sets pm to the return value of ul.release(). Else ul.owns_lock() returns true, and in this case if ul.mutex()->try_unlock_upgrade_and_lock_until(abs_time) returns true, sets pm to the value returned by ul.release() and sets owns to true. [Note: If ul.owns_lock() returns true and ul.mutex()->try_unlock_upgrade_and_lock_until(abs_time) returns false, ul is not modified. — end note]

Throws: Nothing.

template <class Rep, class Period>
    unique_lock(upgrade_lock<mutex_type>&& ul,
                const chrono::duration<Rep, Period>& rel_time);

Requires: The supplied Mutex type shall implement try_unlock_upgrade_and_lock_for() ([thread.upgrademutex.class]).

Effects: Constructs an object of type unique_lock, initializing pm with nullptr and owns with false. If ul.owns_lock() returns false, sets pm to the return value of ul.release(). Else ul.owns_lock() returns true, and in this case if ul.mutex()->try_unlock_upgrade_and_lock_for(rel_time) returns true, sets pm to the value returned by ul.release() and sets owns to true. [Note: If ul.owns_lock() returns true and ul.mutex()->try_unlock_upgrade_and_lock_for(rel_time) returns false, ul is not modified. — end note]

Throws: Nothing.

Add a new section 30.4.2.3 Class template shared_lock [thread.lock.shared]:

Class template shared_lock [thread.lock.shared]

namespace std {

template <class Mutex>
class shared_lock
{
public:
    typedef Mutex mutex_type;

    // Shared locking

    shared_lock() noexcept;
    explicit shared_lock(mutex_type& m);  // blocking
    shared_lock(mutex_type& m, defer_lock_t) noexcept;
    shared_lock(mutex_type& m, try_to_lock_t);
    shared_lock(mutex_type& m, adopt_lock_t);
    template <class Clock, class Duration>
        shared_lock(mutex_type& m,
                    const chrono::time_point<Clock, Duration>& abs_time);
    template <class Rep, class Period>
        shared_lock(mutex_type& m,
                    const chrono::duration<Rep, Period>& rel_time);
    ~shared_lock();

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

    shared_lock(shared_lock&& u) noexcept;
    shared_lock& operator=(shared_lock&& u) noexcept;

    void lock();  // blocking
    bool try_lock();
    template <class Rep, class Period>
        bool try_lock_for(const chrono::duration<Rep, Period>& rel_time);
    template <class Clock, class Duration>
        bool
        try_lock_until(const chrono::time_point<Clock, Duration>& abs_time);
    void unlock();

    // Conversion from upgrade locking

    explicit shared_lock(upgrade_lock<mutex_type>&& u);

    // Conversion from exclusive locking

    explicit shared_lock(unique_lock<mutex_type>&& u);

    // Setters

    void swap(shared_lock& u) noexcept;
    mutex_type* release() noexcept;

    // Getters

    bool owns_lock() const noexcept;
    explicit operator bool () const noexcept;
    mutex_type* mutex() const noexcept;

private:
    mutex_type* pm; // exposition only
    bool owns;      // exposition only
};

template <class Mutex>
  void swap(shared_lock<Mutex>& x, shared_lock<Mutex>& y) noexcept;

}  // std

An object of type shared_lock controls the shared ownership of a lockable object within a scope. Shared ownership of the lockable object may be acquired at construction or after construction, and may be transferred, after acquisition, to another shared_lock object. Objects of type shared_lock are not copyable but are movable. The behavior of a program is undefined if the contained pointer pm is not null and the lockable object pointed to by pm does not exist for the entire remaining lifetime (3.8) of the shared_lock object. The supplied Mutex type shall meet the shared mutex requirements ([thread.sharedmutex.requirements]).

[Note: shared_lock<Mutex> meets the TimedLockable requirements (30.2.5.4). — end note]

shared_lock constructors, destructor, and assignment [thread.lock.shared.cons]
shared_lock() noexcept;

Effects: Constructs an object of type shared_lock.

Postconditions: pm == nullptr and owns == false.

explicit shared_lock(mutex_type& m);

Requires: The calling thread does not own the mutex for any ownership mode.

Effects: Constructs an object of type shared_lock and calls m.lock_shared().

Postconditions: pm == &m and owns == true.

shared_lock(mutex_type& m, defer_lock_t) noexcept;

Effects: Constructs an object of type shared_lock.

Postconditions: pm == &m and owns == false.

shared_lock(mutex_type& m, try_to_lock_t);

Requires: The calling thread does not own the mutex for any ownership mode.

Effects: Constructs an object of type shared_lock and calls m.try_lock_shared().

Postconditions: pm == &m and owns == res where res is the value returned by the call to m.try_lock_shared().

shared_lock(mutex_type& m, adopt_lock_t);

Requires: The calling thread has shared ownership of the mutex.

Effects: Constructs an object of type shared_lock.

Postconditions: pm == &m and owns == true.

template <class Clock, class Duration>
    shared_lock(mutex_type& m,
                const chrono::time_point<Clock, Duration>& abs_time);

Requires: The calling thread does not own the mutex for any ownership mode.

Effects: Constructs an object of type shared_lock and calls m.try_lock_shared_until(abs_time).

Postconditions: pm == &m and owns == res where res is the value returned by the call to m.try_lock_shared_until(abs_time).

template <class Rep, class Period>
    shared_lock(mutex_type& m,
                const chrono::duration<Rep, Period>& rel_time);

Requires: The calling thread does not own the mutex for any ownership mode.

Effects: Constructs an object of type shared_lock and calls m.try_lock_shared_for(rel_time).

Postconditions: pm == &m and owns == res where res is the value returned by the call to m.try_lock_shared_for(rel_time).

~shared_lock();

Effects: If owns calls pm->unlock_shared().

shared_lock(shared_lock&& sl) noexcept;

Postconditions: pm == &sl_p.pm and owns == sl_p.owns (where sl_p is the state of sl just prior to this construction), sl.pm == nullptr and sl.owns == false.

shared_lock& operator=(shared_lock&& sl) noexcept;

Effects: If owns calls pm->unlock_shared().

Postconditions: pm == &sl_p.pm and owns == sl_p.owns (where sl_p is the state of sl just prior to this assignment), sl.pm == nullptr and sl.owns == false.

 explicit shared_lock(upgrade_lock<mutex_type>&& ul);

Effects: If owns calls pm->unlock_upgrade_and_lock_shared().

Postconditions: pm == &ul_p.pm and owns == ul_p.owns (where ul_p is the state of ul just prior to this construction), ul.pm == nullptr and ul.owns == false.

explicit shared_lock(unique_lock<mutex_type>&& ul);

Effects: If owns calls pm->unlock_and_lock_shared().

Postconditions: pm == &ul_p.pm and owns == ul_p.owns (where ul_p is the state of ul just prior to this construction), ul.pm == nullptr and ul.owns == false.

shared_lock locking [thread.lock.shared.locking]
void lock();

Effects: pm->lock_shared().

Postconditions: owns == true.

Throws: Any exception thrown by pm->lock_shared(). system_error if an exception is required (30.2.2). system_error with an error condition of operation_not_permitted if pm is nullptr. system_error with an error condition of resource_deadlock_would_occur if on entry owns is true.

bool try_lock();

Effects: pm->try_lock_shared().

Returns: The value returned by the call to pm->try_lock_shared().

Postconditions: owns == res, where res is the value returned by the call to pm->try_lock_shared().

Throws: Any exception thrown by pm->try_lock_shared(). system_error if an exception is required (30.2.2). system_error with an error condition of operation_not_permitted if pm is nullptr. system_error with an error condition of resource_deadlock_would_occur if on entry owns is true.

template <class Clock, class Duration>
    bool
    try_lock_until(const chrono::time_point<Clock, Duration>& abs_time);

Effects: pm->try_lock_shared_until(abs_time).

Returns: The value returned by the call to pm->try_lock_shared_until(abs_time).

Postconditions: owns == res, where res is the value returned by the call to pm->try_lock_shared_until(abs_time).

Throws: Any exception thrown by pm->try_lock_shared_until(abs_time). system_error if an exception is required (30.2.2). system_error with an error condition of operation_not_permitted if pm is nullptr. system_error with an error condition of resource_deadlock_would_occur if on entry owns is true.

template <class Rep, class Period>
    bool try_lock_for(const chrono::duration<Rep, Period>& rel_time);

Effects: pm->try_lock_shared_for(rel_time).

Returns: The value returned by the call to pm->try_lock_shared_for(rel_time).

Postconditions: owns == res, where res is the value returned by the call to pm->try_lock_shared_for(rel_time).

Throws: Any exception thrown by pm->try_lock_shared_for(rel_time). system_error if an exception is required (30.2.2). system_error with an error condition of operation_not_permitted if pm is nullptr. system_error with an error condition of resource_deadlock_would_occur if on entry owns is true.

void unlock();

Effects: pm->unlock_shared().

Postconditions: owns == false.

Throws: system_error when an exception is required (30.2.2).

Error conditions:

shared_lock modifiers [thread.lock.shared.mod]
void swap(shared_lock& sl) noexcept;

Effects: Swaps the data members of *this and sl.

mutex_type* release() noexcept;

Returns: The previous value of pm.

Postconditions: pm == nullptr and owns == false.

template <class Mutex>
  void swap(shared_lock<Mutex>& x, shared_lock<Mutex>& y) noexcept;

Effects: x.swap(y).

shared_lock observers [thread.lock.shared.obs]
bool owns_lock() const noexcept;

Returns: owns.

explicit operator bool () const noexcept;

Returns: owns.

mutex_type* mutex() const noexcept;

Returns: pm.

Add a new section 30.4.2.4 Class template upgrade_lock [thread.lock.upgrade]:

Class template upgrade_lock [thread.lock.upgrade]

namespace std {

template <class Mutex>
class upgrade_lock
{
public:
    typedef Mutex mutex_type;

    // Upgrade locking

    upgrade_lock() noexcept;
    explicit upgrade_lock(mutex_type& m);  // blocking
    upgrade_lock(mutex_type& m, defer_lock_t) noexcept;
    upgrade_lock(mutex_type& m, try_to_lock_t);
    upgrade_lock(mutex_type& m, adopt_lock_t);
    template <class Clock, class Duration>
        upgrade_lock(mutex_type& m,
                     const chrono::time_point<Clock, Duration>& abs_time);
    template <class Rep, class Period>
        upgrade_lock(mutex_type& m,
                     const chrono::duration<Rep, Period>& rel_time);
    ~upgrade_lock();

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

    upgrade_lock(upgrade_lock&& u) noexcept;
    upgrade_lock& operator=(upgrade_lock&& u) noexcept;

    void lock();  // blocking
    bool try_lock();
    template <class Rep, class Period>
        bool try_lock_for(const chrono::duration<Rep, Period>& rel_time);
    template <class Clock, class Duration>
        bool
        try_lock_until(const chrono::time_point<Clock, Duration>& abs_time);
    void unlock();

    // Conversion from shared locking

    upgrade_lock(shared_lock<mutex_type>&& u, try_to_lock_t);
    template <class Clock, class Duration>
        upgrade_lock(shared_lock<mutex_type>&& u,
                     const chrono::time_point<Clock, Duration>& abs_time);
    template <class Rep, class Period>
        upgrade_lock(shared_lock<mutex_type>&& u,
                     const chrono::duration<Rep, Period>& rel_time);

    // Conversion from exclusive locking

    explicit upgrade_lock(unique_lock<mutex_type>&& u);

    // Setters

    void swap(upgrade_lock& u) noexcept;
    mutex_type* release() noexcept;

    // Getters

    bool owns_lock() const noexcept;
    explicit operator bool () const noexcept;
    mutex_type* mutex() const noexcept;
};

template <class Mutex>
  void swap(upgrade_lock<Mutex>& x, upgrade_lock<Mutex>& y) noexcept;

}  // std

An object of type upgrade_lock controls the upgrade ownership of a lockable object within a scope. Upgrade ownership of the lockable object may be acquired at construction or after construction, and may be transferred, after acquisition, to another upgrade_lock object. Objects of type upgrade_lock are not copyable but are movable. The behavior of a program is undefined if the contained pointer pm is not null and the lockable object pointed to by pm does not exist for the entire remaining lifetime (3.8) of the upgrade_lock object. The supplied Mutex type shall implement the semantics of upgrade mutex ([thread.upgrademutex.class]).

[Note: upgrade_lock<Mutex> meets the TimedLockable requirements (30.2.5.4). — end note]

upgrade_lock constructors, destructor, and assignment [thread.lock.upgrade.cons]
upgrade_lock() noexcept;

Effects: Constructs an object of type upgrade_lock.

Postconditions: pm == nullptr and owns == false.

explicit upgrade_lock(mutex_type& m);

Requires: The calling thread does not own the mutex for any ownership mode.

Effects: Constructs an object of type upgrade_lock and calls m.lock_upgrade().

Postconditions: pm == &m and owns == true.

upgrade_lock(mutex_type& m, defer_lock_t) noexcept;

Effects: Constructs an object of type upgrade_lock.

Postconditions: pm == &m and owns == false.

upgrade_lock(mutex_type& m, try_to_lock_t);

Requires: The calling thread does not own the mutex for any ownership mode.

Effects: Constructs an object of type upgrade_lock and calls m.try_lock_upgrade().

Postconditions: pm == &m and owns == res where res is the value returned by the call to m.try_lock_upgrade().

upgrade_lock(mutex_type& m, adopt_lock_t);

Requires: The calling thread has upgrade ownership of the mutex.

Effects: Constructs an object of type upgrade_lock.

Postconditions: pm == &m and owns == true.

template <class Clock, class Duration>
    upgrade_lock(mutex_type& m,
                const chrono::time_point<Clock, Duration>& abs_time);

Requires: The calling thread does not own the mutex for any ownership mode.

Effects: Constructs an object of type upgrade_lock and calls m.try_lock_upgrade_until(abs_time).

Postconditions: pm == &m and owns == res where res is the value returned by the call to m.try_lock_upgrade_until(abs_time).

template <class Rep, class Period>
    upgrade_lock(mutex_type& m,
                const chrono::duration<Rep, Period>& rel_time);

Requires: The calling thread does not own the mutex for any ownership mode.

Effects: Constructs an object of type upgrade_lock and calls m.try_lock_upgrade_for(rel_time).

Postconditions: pm == &m and owns == res where res is the value returned by the call to m.try_lock_upgrade_for(rel_time).

~upgrade_lock();

Effects: If owns calls pm->unlock_upgrade().

upgrade_lock(upgrade_lock&& ul) noexcept;

Postconditions: pm == &ul_p.pm and owns == ul_p.owns (where ul_p is the state of ul just prior to this construction), ul.pm == nullptr and ul.owns == false.

upgrade_lock& operator=(upgrade_lock&& ul) noexcept;

Effects: If owns calls pm->unlock_upgrade().

Postconditions: pm == &ul_p.pm and owns == ul_p.owns (where ul_p is the state of ul just prior to this assignment), ul.pm == nullptr and ul.owns == false.

upgrade_lock(shared_lock<mutex_type>&& sl, try_to_lock_t);

Effects: Constructs an object of type upgrade_lock, initializing pm with nullptr and owns with false. If sl.owns_lock() returns false, sets pm to the return value of sl.release(). Else sl.owns_lock() returns true, and in this case if sl.mutex()->try_unlock_shared_and_lock_upgrade() returns true, sets pm to the value returned by sl.release() and sets owns to true. [Note: If sl.owns_lock() returns true and sl.mutex()->try_unlock_shared_and_lock_upgrade() returns false, sl is not modified. — end note]

Postconditions: pm == &sl_p.pm and owns == sl_p.owns (where sl_p is the state of sl just prior to this construction), sl.pm == nullptr and sl.owns == false.

template <class Clock, class Duration>
    upgrade_lock(shared_lock<mutex_type>&& sl,
                 const chrono::time_point<Clock, Duration>& abs_time);

Effects: Constructs an object of type upgrade_lock, initializing pm with nullptr and owns with false. If sl.owns_lock() returns false, sets pm to the return value of sl.release(). Else sl.owns_lock() returns true, and in this case if sl.mutex()->try_unlock_shared_and_lock_upgrade_until(abs_time) returns true, sets pm to the value returned by sl.release() and sets owns to true. [Note: If sl.owns_lock() returns true and sl.mutex()->try_unlock_shared_and_lock_upgrade_until(abs_time) returns false, sl is not modified. — end note]

Postconditions: pm == &sl_p.pm and owns == sl_p.owns (where sl_p is the state of sl just prior to this construction), sl.pm == nullptr and sl.owns == false.

template <class Rep, class Period>
    upgrade_lock(shared_lock<mutex_type>&& sl,
                 const chrono::duration<Rep, Period>& rel_time);

Effects: Constructs an object of type upgrade_lock, initializing pm with nullptr and owns with false. If sl.owns_lock() returns false, sets pm to the return value of sl.release(). Else sl.owns_lock() returns true, and in this case if sl.mutex()->try_unlock_shared_and_lock_upgrade_for(rel_time) returns true, sets pm to the value returned by sl.release() and sets owns to true. [Note: If sl.owns_lock() returns true and sl.mutex()->try_unlock_shared_and_lock_upgrade_for(rel_time) returns false, sl is not modified. — end note]

Postconditions: pm == &sl_p.pm and owns == sl_p.owns (where sl_p is the state of sl just prior to this construction), sl.pm == nullptr and sl.owns == false.

explicit upgrade_lock(unique_lock<mutex_type>&& ul);

Effects: If owns calls pm->unlock_and_lock_upgrade().

Postconditions: pm == &ul_p.pm and owns == ul_p.owns (where ul_p is the state of ul just prior to this construction), ul.pm == nullptr and ul.owns == false.

upgrade_lock locking [thread.lock.upgrade.locking]
void lock();

Effects: pm->lock_upgrade().

Postconditions: owns == true.

Throws: Any exception thrown by pm->lock_upgrade(). system_error if an exception is required (30.2.2). system_error with an error condition of operation_not_permitted if pm is nullptr. system_error with an error condition of resource_deadlock_would_occur if on entry owns is true.

bool try_lock();

Effects: pm->try_lock_upgrade().

Returns: The value returned by the call to pm->try_lock_upgrade().

Postconditions: owns == res, where res is the value returned by the call to pm->try_lock_upgrade().

Throws: Any exception thrown by pm->try_lock_upgrade(). system_error if an exception is required (30.2.2). system_error with an error condition of operation_not_permitted if pm is nullptr. system_error with an error condition of resource_deadlock_would_occur if on entry owns is true.

template <class Clock, class Duration>
    bool
    try_lock_until(const chrono::time_point<Clock, Duration>& abs_time);

Effects: pm->try_lock_upgrade_until(abs_time).

Returns: The value returned by the call to pm->try_lock_upgrade_until(abs_time).

Postconditions: owns == res, where res is the value returned by the call to pm->try_lock_upgrade_until(abs_time).

Throws: Any exception thrown by pm->try_lock_upgrade_until(abs_time). system_error if an exception is required (30.2.2). system_error with an error condition of operation_not_permitted if pm is nullptr. system_error with an error condition of resource_deadlock_would_occur if on entry owns is true.

template <class Rep, class Period>
    bool try_lock_for(const chrono::duration<Rep, Period>& rel_time);

Effects: pm->try_lock_upgrade_for(rel_time).

Returns: The value returned by the call to pm->try_lock_upgrade_for(rel_time).

Postconditions: owns == res, where res is the value returned by the call to pm->try_lock_upgrade_for(rel_time).

Throws: Any exception thrown by pm->try_lock_upgrade_for(rel_time). system_error if an exception is required (30.2.2). system_error with an error condition of operation_not_permitted if pm is nullptr. system_error with an error condition of resource_deadlock_would_occur if on entry owns is true.

void unlock();

Effects: pm->unlock_upgrade().

Postconditions: owns == false.

Throws: system_error when an exception is required (30.2.2).

Error conditions:

upgrade_lock modifiers [thread.lock.upgrade.mod]
void swap(upgrade_lock& sl) noexcept;

Effects: Swaps the data members of *this and sl.

mutex_type* release() noexcept;

Returns: The previous value of pm.

Postconditions: pm == nullptr and owns == false.

template <class Mutex>
  void swap(upgrade_lock<Mutex>& x, upgrade_lock<Mutex>& y) noexcept;

Effects: x.swap(y).

upgrade_lock observers [thread.lock.upgrade.obs]
bool owns_lock() const noexcept;

Returns: owns.

explicit operator bool () const noexcept;

Returns: owns.

mutex_type* mutex() const noexcept;

Returns: pm.