Document Number: | N4033 |
Date: | 2014-05-23 |
Author: | Anthony
Williams Just Software Solutions Ltd |
synchronized_value<T>
for associating a mutex with a valueA couple of years ago I wrote
an article
for Dr Dobb's Journal discussing a synchronized_value
template to associate a mutex with a value. I'd like to
propose that template for standardization, with a few
modifications.
The basic idea is that synchronized_value<T>
stores a value of type T
and a mutex. It then exposes a
pointer interface, such that derefencing the pointer yields a
special wrapper type that holds a lock on the mutex, and that can be
implicitly converted to T
for reading, and which
forwards any values assigned to the assignment operator of the
underlying T
for writing. There is also an arrow
operator which allows member functions on the wrapped value to be
called. e.g.
synchronized_value<std::string> s; std::string readValue() { return *s; } void setValue(std::string const& newVal) { *s=newVal; } void appendToValue(std::string const& extra) { s->append(extra); }
All three of these functions can be called by multiple threads concurrently, and the implicit mutex locks will ensure that the calls are serialized so there are no data races.
The proposed interface also provides a
separate update_guard<T>
template that locks the
mutex in a synchronized_value<T>
object for a
longer period, thus providing the ability to perform multiple
operations under a single lock. This can be important for things
like message queues, where you want to check if the queue has a
value before trying to pop from the queue.
synchronized_value<std::queue<message_type>> queue; void process_message(){ std::optional<message_type> local_message; { update_guard<std::queue<message_type>> guard(queue); if(!guard->empty()){ local_message.emplace(guard->front()); guard->pop_front(); } else return; } do_processing(local_message.value()); }
Finally, this proposal adds an apply()
member function
to the synchronized_value<T>
template, which
locks the mutex, and passes a reference to the stored value to the
supplied function. The previous example could thus be written as follows:
synchronized_value<std::queue<message_type>> queue; void process_message(){ std::optional<message_type> local_message; queue.apply([&](std::queue<message_type>& q){ if(!q.empty()){ local_message.emplace(q.front()); q.pop_front(); } } if(local_message) do_processing(local_message.value()); }
Add a new section to chapter 30 as follows.
30.x Synchronized Values
This section describes a class template to associate a mutex (30.4) with a value in order to facilitate the construction of race-free programs.
Header <synchronized_value> synopsis
namespace std { template<typename T> class synchronized_value; template<typename T> class update_guard; }30.x.1 Class template
synchronized_value
namespace std { template<typename T> class synchronized_value { public: synchronized_value(synchronized_value const&) = delete; synchronized_value& operator=(synchronized_value const&) = delete; template<typename ... Args> synchronized_value(Args&& ... args); ~synchronized_value(); template<typename F> auto apply(F&& func) -> typename std::result_of<F(T&)>::type; unspecified operator->(); unspecified operator*(); }; }An object of type
synchronized_value<T>
wraps an object of typeT
along with a mutex to ensure that only one thread can access the wrapped object at a time. The wrapped object can be accessed through the pointer dereference and member access operators, through an instance of the std::update_guard class template, or by passing a function or callable object to theapply
member function. All such accesses are done with the internal mutex locked.
template<typename ... Args> synchronized_value(Args&& ... args);
- Requires:
T
is constructible fromargs
- Effects:
- Constructs a
std::synchronized_value
instance containing an object constructed withT(std::forward<Args>(args)...)
. If no arguments are supplied then the wrapped object is default-constructed.- Throws:
- Any exceptions thrown by the construction of the wrapped object.
~synchronized_value();
- Effects:
- Destroys
*this
and the contained object of typeT
.
template<typename F> auto apply(F&& func) -> typename std::result_of<F(T&)>::type;
- Effects:
- Locks the internal mutex, calls
func(t)
, wheret
is the wrapped object of typeT
store in*this
, then unlocks the internal mutex.- Returns:
- The return value of the call to
func
.- Throws:
std::system_error
if the lock could not be acquired. Any exceptions thrown by the call tofunc(t)
. Note: the internal mutex is unlocked after the call, even iffunc(t)
exits with an exception.- Synchronization:
- Multiple threads may call
apply()
,operator->()
oroperator*()
on the same instance ofsynchronized_value
concurrently without external synchronization. If multiple threads callapply()
,operator->()
oroperator*()
concurrently on the same instance then the behaviour is as-if they each made their call in some unspecified order. The completion of the full expression associated with one access synchronizes-with a subsequent access to the wrapped object through*this
.
unspecified operator->();
- Requires:
- Given an object
sv
of typestd::synchronized_value<T>
, and an objectp
of typeT*
,sv->some-expr
is valid if and only ifp->some-expr
would be valid.- Effects:
- Locks the internal mutex associated with
*this
and returns an object that implements the member access operator to access the wrappedT
object. Unlocks the internal mutex at the end of the full expression.- Note:
- Multiple accesses to the same
synchronized_value
object within the same full expression will lead to deadlock.- Throws:
std::system_error
if the lock could not be acquired.- Synchronization:
- Multiple threads may call
apply()
,operator->()
oroperator*()
on the same instance ofsynchronized_value
concurrently without external synchronization. If multiple threads callapply()
,operator->()
oroperator*()
concurrently on the same instance then the behaviour is as-if they each made their call in some unspecified order. The completion of the full expression associated with one access synchronizes-with a subsequent access to the wrapped object through*this
.
unspecified operator*();
- Effects:
Locks the internal mutex associated with
*this
and returns an object that provides access to the wrappedT
object. Unlocks the internal mutex at the end of the full expression.The expression
*sv=x
assigns the valuex
to the wrapped object, and requires thatT
is MoveAssignable fromx
.
*sv
is implicitly convertible toT
. Such a conversion copy-constructs a newT
object from the stored value, and thus requires thatT
is CopyConstructible.- Note:
- Multiple accesses to the same
synchronized_value
object within the same full expression will lead to deadlock.- Throws:
std::system_error
if the lock could not be acquired.- Synchronization:
- Multiple threads may call
apply()
,operator->()
oroperator*()
on the same instance ofsynchronized_value
concurrently without external synchronization. If multiple threads callapply()
,operator->()
oroperator*()
concurrently on the same instance then the behaviour is as-if they each made their call in some unspecified order. The completion of the full expression associated with one access synchronizes-with a subsequent access to the wrapped object through*this
.30.x.2
update_guard
Class Templatenamespace std { template <class T> class update_guard { public: explicit update_guard(synchronized_value<T>& sv); ~update_guard(); T& operator*() noexcept; T* operator->() noexcept; update_guard(update_guard const& ) = delete; update_guard& operator=(update_guard const& ) = delete; }; }An instance of
update_guard
locks the internal mutex of the suppliedsynchronized_value
object for the lifetime of theupdate_guard
object. It provides a means of accessing the stored value multiple times without releasing and reacquiring the lock.
update_guard(synchronized_value& sv);
- Effects:
- Constructs a new
update_guard
withsv
as the associatedstd::synchronized_value
instance. Locks the mutex forsv
for the current thread.- Throws:
std::system_error
if the lock could not be acquired.
~update_guard();
- Effects:
- Destroys
*this
and unlocks the mutex for the associatedsynchronized_value
object.
T* operator->();
- Returns:
- A pointer to the object of type
T
stored in the associatedsynchronized_value
object.
T& operator*();
- Returns:
- A reference to the object of type
T
stored in the associatedsynchronized_value
object.