Document Number: | P0290R3 |
Date: | 2023-01-06 |
Author: | Anthony Williams |
Audience: | SG1 |
apply()
for synchronized_value<T>
This paper is a followup to P0290R2, based on feedback from the Kona 2022 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.
The name is chosen to fit in with std::apply
for tuples,
since the operation is conceptually similar. Rather than expanding
a std::tuple
to supply the arguments to the function, the
values wrapped by the synchronized_value
s are extracted to
supply the arguments to the function.
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 provide locked access to a value in order to facilitate the construction of race-free programs.
Header <synchronized_value> synopsis
namespace std { template<class T> class synchronized_value; template<class F,class FirstValue,class ... OtherValues> decltype(auto) apply( F&& f,synchronized_value<FirstValue>& first_value,synchronized_value<ValueTypes>&... other_values); }30.x.1 Class template
synchronized_value
namespace std { template<class T> class synchronized_value { public: synchronized_value(synchronized_value const&) = delete; synchronized_value& operator=(synchronized_value const&) = delete; template<class ... Args> synchronized_value(Args&& ... args); ~synchronized_value(); private: T __value; // exposition only std::mutex __mut; // exposition only }; }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<class ... Args> synchronized_value(Args&& ... args);
- Constraints:
is_constructible_v<T,Args...>
istrue
- Effects:
- Direct-non-list-initializes the contained value with
std::forward<Args>(args)...
.- Throws:
- Any exceptions thrown by the selected constructor of
T
.
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<class F,class FirstValue,class ... OtherValues> decltype(auto) apply( F&& f,synchronized_value<FirstValue>& first_value,synchronized_value<ValueTypes>&... other_values);
- Effects:
- Equivalent to:
scoped_lock lock(first_value.__mut,other_values.__mut...); return INVOKE(std::forward<F>(f),first_value.__value,other_values.__value...);- Returns:
- The return value of the invocation of
f
.- Throws:
std::system_error
if there was an error acquiring any of the locks. Any exceptions thrown by the invocation off
.- 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 invocation ofapply
synchronizes-with a subsequent invocation ofapply
where the same instance ofsynchronized_value
is passed to both invocations ofapply
.- Requires:
- A single instance of
synchronized_value
shall not be passed more than once to the same invocation ofapply
. [Example:synchronized_value<int> sv; void f(int,int); apply(f,sv,sv); // undefined behaviour, sv passed more than once to same call—End Example]
The invocation off
shall not callapply
directly or indirectly passing any offirst_value, other_values...
.
Following discussion in LWG in Kona, the following changes have been made:
Following discussion in LEWG in Kona, the following changes have been made:
std::apply
for
tuples, and is a better match when a synchronized_value
is supplied.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
.