Document Number:N4033
Date:2014-05-23
Author:Anthony Williams
Just Software Solutions Ltd

N4033: synchronized_value<T> for associating a mutex with a value

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

Proposed Wording

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 type T 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 the apply member function. All such accesses are done with the internal mutex locked.

template<typename ... Args>
synchronized_value(Args&& ... args);
Requires:
T is constructible from args
Effects:
Constructs a std::synchronized_value instance containing an object constructed with T(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 type T.
template<typename F>
auto apply(F&& func) -> typename std::result_of<F(T&)>::type;
Effects:
Locks the internal mutex, calls func(t), where t is the wrapped object of type T 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 to func(t). Note: the internal mutex is unlocked after the call, even if func(t) exits with an exception.
Synchronization:
Multiple threads may call apply(), operator->() or operator*() on the same instance of synchronized_value concurrently without external synchronization. If multiple threads call apply(), operator->() or operator*() 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 type std::synchronized_value<T>, and an object p of type T*, sv->some-expr is valid if and only if p->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 wrapped T 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->() or operator*() on the same instance of synchronized_value concurrently without external synchronization. If multiple threads call apply(), operator->() or operator*() 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 wrapped T object. Unlocks the internal mutex at the end of the full expression.

The expression *sv=x assigns the value x to the wrapped object, and requires that T is MoveAssignable from x.

*sv is implicitly convertible to T. Such a conversion copy-constructs a new T object from the stored value, and thus requires that T 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->() or operator*() on the same instance of synchronized_value concurrently without external synchronization. If multiple threads call apply(), operator->() or operator*() 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 Template

namespace 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 supplied synchronized_value object for the lifetime of the update_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 with sv as the associated std::synchronized_value instance. Locks the mutex for sv 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 associated synchronized_value object.
T* operator->();
Returns:
A pointer to the object of type T stored in the associated synchronized_value object.
T& operator*();
Returns:
A reference to the object of type T stored in the associated synchronized_value object.