| 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_valuenamespace 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 typeTalong 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 theapplymember function. All such accesses are done with the internal mutex locked.
template<typename ... Args> synchronized_value(Args&& ... args);
- Requires:
Tis constructible fromargs- Effects:
- Constructs a
std::synchronized_valueinstance 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
*thisand 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), wheretis the wrapped object of typeTstore in*this, then unlocks the internal mutex.- Returns:
- The return value of the call to
func.- Throws:
std::system_errorif 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_valueconcurrently 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
svof typestd::synchronized_value<T>, and an objectpof typeT*,sv->some-expris valid if and only ifp->some-exprwould be valid.- Effects:
- Locks the internal mutex associated with
*thisand returns an object that implements the member access operator to access the wrappedTobject. Unlocks the internal mutex at the end of the full expression.- Note:
- Multiple accesses to the same
synchronized_valueobject within the same full expression will lead to deadlock.- Throws:
std::system_errorif the lock could not be acquired.- Synchronization:
- Multiple threads may call
apply(),operator->()oroperator*()on the same instance ofsynchronized_valueconcurrently 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
*thisand returns an object that provides access to the wrappedTobject. Unlocks the internal mutex at the end of the full expression.The expression
*sv=xassigns the valuexto the wrapped object, and requires thatTis MoveAssignable fromx.
*svis implicitly convertible toT. Such a conversion copy-constructs a newTobject from the stored value, and thus requires thatTis CopyConstructible.- Note:
- Multiple accesses to the same
synchronized_valueobject within the same full expression will lead to deadlock.- Throws:
std::system_errorif the lock could not be acquired.- Synchronization:
- Multiple threads may call
apply(),operator->()oroperator*()on the same instance ofsynchronized_valueconcurrently 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_guardClass 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_guardlocks the internal mutex of the suppliedsynchronized_valueobject for the lifetime of theupdate_guardobject. 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_guardwithsvas the associatedstd::synchronized_valueinstance. Locks the mutex forsvfor the current thread.- Throws:
std::system_errorif the lock could not be acquired.
~update_guard();
- Effects:
- Destroys
*thisand unlocks the mutex for the associatedsynchronized_valueobject.
T* operator->();
- Returns:
- A pointer to the object of type
Tstored in the associatedsynchronized_valueobject.
T& operator*();
- Returns:
- A reference to the object of type
Tstored in the associatedsynchronized_valueobject.