Document number: N3659
Revision/subset of: N3568

Howard Hinnant
Detlef Vollmann
Hans Boehm
2013-04-19

Shared locking in C++

Revision 2

Status

N3568 was presented at the Spring 2013 meeting in Bristol to SG1 (Concurrency and Parallelism). The decision was to bring shared_mutex only forward for C++14. Also the specification should allow for spurious failures.

Changes compared to N3568

This paper only includes the proposed wording. For background please refer to N3568. upgrade_mutex and upgrade_lock was removed. Formal wording was added to allow for spurious failures. Editorial note: Defining shared mutex types (plural) while we are only providing one singe type (shared_mutex may sound strange. However, the text is written as requirements, and (implementations or) programmers may provide additional types that fulfill these requirements. For the other mutex types we don't have the problem as we always have more than one type for a specific requirement set.

Proposed Wording

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

Header <shared_mutex> synopsis

namespace std {

class shared_mutex;
template <class Mutex> class shared_lock;
template <class Mutex>
  void swap(shared_lock<Mutex>& x, shared_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 and std::shared_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 and std::shared_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 standard library type std::shared_mutex is a shared mutex type. Shared mutex types 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.

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() 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. An implementation may fail to obtain the lock even if it is not held by any other thread.

Return type: bool.

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

Synchronization: If try_lock_shared() returns true, prior unlock() 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. [ Note: As with try_lock(), there is no guarantee that ownership will be obtained if the lock is available, but implementations are expected to make a strong effort to do so. --end note ]

Return type: bool.

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

Synchronization: If try_lock_shared_for() returns true, prior unlock() 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. [ Note: As with try_lock(), there is no guarantee that ownership will be obtained if the lock is available, but implementations are expected to make a strong effort to do so. --end note ]

Return type: bool.

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

Synchronization: If try_lock_shared_until() returns true, prior unlock() 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:

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();

    // 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.

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.