Document Number: | P0290!R1 |
Date: | 2016-06-23 |
Author: | Anthony
Williams Just Software Solutions Ltd |
Audience: | SG1 |
apply()
for synchronized_value<T>
This paper is a followup to N4033, based on feedback from the Rapperswil 2014 meeting.
The basic idea is that synchronized_value<T>
stores a value of
typeT
and a mutex.The apply()
function then provides a means of
accessing the stored value with the mutex locked, automatically unlocking the mutex
afterwards.
The apply()
function is variadic, so you can operate on a set
of synchronized_value<T>
objects. All the mutexes are locked prior to
invoking the supplied callable object, and then they are all released afterwards.
This provides an easy way for developers to ensure that all accesses to a given object are done with the relevant mutex locked, whilst also allowing for operations that require locks on multiple objects.
In order to avoid simple mistakes when using the synchronized_value<T>
objects, there are no public member functions or operations other than construction.
The actual implementation may use an alternative synchronization mechanism instead of a mutex, provided that the synchronization requirements are met.
Simple accesses can be done with simple lambdas:
synchronized_value<std::string> s; std::string read_value(){ return apply([](auto& x){return x;},s); } void set_value(std::string const& new_val){ apply([&](auto& x){x=new_val;},s); }
More complex processing can be done with a more complex lambda, or a separate function or callable object:
synchronized_value<std::queue<message_type>> queue; void process_message(){ std::optional<message_type> local_message; apply([&](std::queue<message_type>& q){ if(!q.empty()){ local_message.emplace(std::move(q.front())); q.pop_front(); } },queue); if(local_message) do_processing(local_message.value()); }
The variadic nature of apply()
means that writing code that accesses
multiple synchronized_value<T>
objects is straightforward. It uses the same
mechanism as std::lock()
to ensure that the requisite mutexes are locked without
deadlock.
The ubiquitous example of transferring money between accounts can then be simply written as a follows:
void transfer_money( synchronized_value<account>& from_, synchronized_value<account>& to_, money_value amount){ apply([=](auto& from,auto& to){ from.withdraw(amount); to.deposit(amount); },from_,to_); }
Add a new section to chapter 30 as follows.
30.x Synchronized Values
This section describes a class template to provided locked access to 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 F,typename ... ValueTypes> decltype(std::declval()(std::declval ()...)) apply( F&& f,synchronized_value<ValueTypes>&... values); } 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(); }; }An object of type
synchronized_value<T>
wraps an object of typeT
. The wrapped object can be accessed by passing a callable object or function toapply
. All such accesses are done with a lock held to ensure that only one thread may be accessing the wrapped object for a givensynchronized_value
at a time.
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.
std::system_error
if any necessary resources cannot be acquired.
~synchronized_value();
- Effects:
- Destroys the contained object of type
T
and*this
.30.x.2
apply
function
template<typename F,typename ... ValueTypes> decltype(std::declval()(std::declval ()...)) apply( F&& f,synchronized_value<ValueTypes>&... values);
- Effects:
- Acquires the lock for each Vi in
values
using the algorithm from [thread.lock.algorithm] to ensure that deadlock does not occur whenapply
is called from multiple threads of execution. Then invokesfunc(data...)
, where eachdatai
is the wrapped object of typeValueTypei
stored in Vi, and returns the result to the caller. When the invocation offunc
returns or exits via exception, then any internal locks that were acquired for this call toapply
are released prior to control leavingapply
.- Returns:
- The return value of the call to
func
.- Throws:
std::system_error
if any of the locks could not be acquired. Any exceptions thrown by the invocation offunc(data...)
. Note: the locks are released after the call, even ifapply
exits with an exception.- Synchronization:
- Multiple threads may call
apply()
concurrently without external synchronization. If multiple threads callapply()
concurrently passing the same instance(s) ofsynchronized_value
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 same object.- Requires:
- A single instance of
synchronized_value
shall not be passed more than once to the same invocation ofapply
. [Example:synchronized_value—End Example]sv; void f(int,int); apply(f,sv,sv); // undefined behaviour, sv passed more than once to same call
The function or callable objectf
shall not callapply
directly or indirectly passing any of thesynchronized_value
objects invalues
.
Following discussion in SG1 in Oulu, the following changes have been made:
apply
with the same synchronized_value
more than once in
the same argument list is explicitly disallowed;apply
with an overlapping set
of synchronized_value
objects is explicitly disallowed; andsynchronized_value
may throw std::system_error
.