Thread Safe CachedValue<> for Reading on other threads

I wanted to share some code for thread-safe read-only versions of juce::CachedValue and would appreciate any feedback as I may have some misunderstanding.
(See main code towards bottom.)

None of the following solutions would allow for thread-safe writes to juce::CachedValue from threads other than the Message Thread, only reads from CachedValue.

The Context

Recently, I have been wanting to use CachedValue in a thread-safe manner so that I may read some CachedValue data linked to a ValueTree from a separate OpenGL thread. I’ve got a ValueTree with data that is being set by UI elements such as juce::Slider, etc, and I want the OpenGL thread to be able to receive any updates to the ValueTree data via CachedValue so that the GL thread can render different visuals depending on the values of the data.

The only problem with this approach is that CachedValue is not thread-safe for use outside of the JUCE Message Thread, but I want to access CachedValue data from a GL thread. The JUCE Message thread and GL thread are not synchronized automatically via the JUCE library for my case of using a custom OpenGLRenderer [1]. Therefore, I need some thread-safe solution to read from any number of CachedValues from a separate thread.

Research

In @dave96’s ADC’17 talk on ValueTree, he provides a thread-safe AtomicWrapper type that can be used with CachedValue for reading CachedValue data from other threads. But, the only supported data types are primitives (those that work with std::atomic).

See Dave’s talk here including his AtomicWrapper code which has been copied lower down in this post.

Declaring a CachedValue using an AtomicWrapper looks like:

juce::CachedValue<AtomicWrapper<int>> cachedCount;

Then you could attach it to a ValueTree for the CachedValue to automatically receive synchronous updates from changes (such as moving a UI slider):

cachedCount.referTo (myValueTree, countIDInValueTree, nullptr);

And then a separate thread, like the GL thread, could read from this safely:

void MyOpenGLRenderer::renderOpenGL()
{
    int count = cachedCount.get(); // Thread-safe read, we just cannot write to cachedCount
    
    // Render `count` number of 3D cows or something
    for (int i = 0; i < count; ++i)
    {
        draw3DCow();
    }
}

This is great but only works for primitive data types (int, float, etc.) that work with std::atomic.

To support other templated CachedValue types, such as juce::Colour, I thought it might be beneficial to follow Dave’s pattern by creating a thread-safe read-only data wrapper that uses a std::mutex to protect the internal data. So, I call this alternative wrapper, MutexWrapper.

To use a MutexWrapper with CachedValue, a declaration would look like:

juce::CachedValue<MutexWrapper<juce::Colour>> cachedColour;

The attachment to the ValueTree with the referTo() call would look similar to above call for cachedCount.

And then a separate thread, like the GL thread, could read from this safely:

void MyOpenGLRenderer::renderOpenGL()
{
     // Thread-safe read, we just cannot write to cachedColour
    juce::Colour colour = cachedColour.get();
    
    // Render a cow with a specific colour
    draw3DCowWithColour (colour);
}

The Wrapper Code

Dave Rowland’s AtomicWrapper for thread-safe reading of a CachedValue templated with a primitive data type:

template<typename Type>
struct AtomicWrapper
{
    AtomicWrapper() = default;
    
    template<typename OtherType>
    AtomicWrapper (const OtherType& other)
    {
        value.store (other);
    }
 
    AtomicWrapper (const AtomicWrapper& other)
    {
        value.store (other.value);
    }
    
    AtomicWrapper& operator= (const AtomicWrapper& other) noexcept
    {
        value.store (other.value);
        return *this;
    }
    
    bool operator== (const AtomicWrapper& other) const noexcept
    {
        return value.load() == other.value.load();
    }
    
    bool operator!= (const AtomicWrapper& other) const noexcept
    {
        return value.load() != other.value.load();
    }
    
    operator var() const noexcept  { return value.load(); }
    operator Type() const noexcept { return value.load(); }
 
    std::atomic<Type> value { Type() };
};

My MutexWrapper for thread-safe reading of a CachedValue templated with any data type:

template<typename Type>
class MutexWrapper
{
public:
    MutexWrapper() = default;
    
    /** Allows conversion from var (used internally in ValueTree) to Type, but
        requires there to be an implemented juce::VariantConverter for any Type
        you are using. See an implementation of VariantConverter for
        juce::Color below.
      */
    MutexWrapper (const juce::var& other)
    {
        std::scoped_lock lock (mLock);
        value = juce::VariantConverter<Type>::fromVar (other);
    }
 
    MutexWrapper (const MutexWrapper& other)
    {
        jassert (this != &other);
        std::scoped_lock lock (mLock);
        value = other.getValue();
    }
    
    MutexWrapper& operator= (const MutexWrapper& other) noexcept
    {
        jassert (this != &other);
        std::scoped_lock lock (mLock);
        value = other.getValue();
        return *this;
    }
    
    bool operator== (const MutexWrapper& other) const noexcept
    {
        jassert (this != &other);
        std::scoped_lock lock (mLock);
        return value == other.getValue();
    }
    
    bool operator!= (const MutexWrapper& other) const noexcept
    {
        jassert (this != &other);
        std::scoped_lock lock (mLock);
        return value != other.getValue();
    }
    
    operator var() const noexcept  { return getValue(); }
    operator Type() const noexcept { return getValue(); }
    
    Type getValue() const
    {
        std::scoped_lock lock (mLock);
        return value;
    }
 
 private:
    Type value { Type() };
    
    /** Synchronizes access to value.
 
        @note It is marked mutable because mutexes are thread-safe, and we want
              the mutex to be used (mutated) even in const methods such as
              getValue() to protect access of value.
        
        Taken directly from Herb Sutter's C++ and Beyong 2012 talk about const
        and mutable in C++ 11 titled "You don't know [blank] and [blank]":
        https://channel9.msdn.com/posts/C-and-Beyond-2012-Herb-Sutter-You-dont-know-blank-and-blank
    */
    mutable std::mutex mLock;
};

For MutexWrapper to work properly, whatever type you chose to store in it must have a VariantConverter implemented in the juce namespace. You may have a file of various VariantConverters for the data types you want to use such as VariantConverters.h. And just make sure this file is #included in the file where your MutexWrapper lives.

Example VariantConverters.h:

#pragma once
#include <JuceHeader.h>

/** Provides conversions to and from juce::var (variant).
    
    @note To make VariantConverters accessible for use with juce::var, we need
          to publish them to the juce namespace.
          BUT maybe I am wrong about using juce namespace. I thought this would
          allow the VariantConverters specified in the namespace to automatically
          be used for any necessary juce::var conversions, but in practice for
          myself, this seems not to work and I am forced to manually call fromVar
          and toVar.
*/
namespace juce
{
/** juce::Colour Variant Converter */
template <>
struct VariantConverter<Colour>
{
    static juce::Colour fromVar (const juce::var& v) { return juce::Colour::fromString (v.toString()); }
    static juce::var toVar (const juce::Colour& c) { return c.toString(); }
};

// Include any other custom VarientConverters for the data types you wish to use
// in a ValueTree ...

} // namespace juce

Final Thoughts

I would think these wrapper classes could be made more syntactic-sugary by creating new classes that inherit from CachedValue but uses the specific wrapper class internally. So you could declare stuff like:

CachedValueAtomicReadOnly<int> cachedCount;
CachedValueMutexReadOnly<Colour> cachedColour;

As a bonus, you could hide all the mutator methods for CachedValue in these derived classes AtomicCachedValue and MutexCachedValue so the user does not accidentally attempt to set the cached value, which would not be a thread safe operation as it would trigger the synchronous ValueTree updates from another thread.

If anyone can tell that I am fundamentally misunderstanding this stuff, I would be very happy to know, thanks!

1 Like

I don’t think you’d have to inherit from CachedValue would you? Wouldn’t it be sufficient to create a couple of template aliases?

template<class T>
using CachedValueAtomicReadOnly = juce::CachedValue<AtomicWrapper<T>>;

template<class T>
using CachedValueMutexReadOnly = juce::CachedValue<MutexWrapper<T>>;
1 Like

Thanks for the reply, Dave!

Yes that solution works great. I thought inheritance might be useful if you wanted to additionally hide any mutating CachedValue methods (like operator=) from these new types so a user does not accidentally set the CachedValue and trigger a ValueTree update from another thread which would not be thread safe (at least in my use-case of such a class).

Maybe something like:

template<class WrapperType>
class CachedValueReadOnly : public CachedValue<WrapperType>
{
public:
    CachedValueReadOnly() : CachedValue<WrapperType>() {}
    CachedValueReadOnly (ValueTree& tree, const Identifier& propertyID,
                         UndoManager* undoManager)
                       : CachedValue<WrapperType> (tree, propertyID, undoManager) {}
    CachedValueReadOnly (ValueTree& tree, const Identifier& propertyID,
                         UndoManager* undoManager, const WrapperType& defaultToUse)
                       : CachedValue<WrapperType> (tree, propertyID, undoManager, defaultToUse) {}
    
    // Hide any public methods that would write to the CachedValue and modify
    // the ValueTree, because if these were called from a thread other than the
    // Message Thread, and your ValueTree was being managed by the Message
    // Thread, then these methods would not be thread safe.
    CachedValue<WrapperType> & operator= (const WrapperType& newValue) = delete;
    void setValue (const WrapperType& newValue, UndoManager* undoManagerToUse) = delete;
    void resetToDefault() = delete;
    void resetToDefault (UndoManager* undoManagerToUse) = delete;

};

template<class Type>
using CachedValueAtomicReadOnly = CachedValueReadOnly<AtomicWrapper<Type>>;

template<class Type>
using CachedValueMutexReadOnly = CachedValueReadOnly<MutexWrapper<Type>>;

But maybe this is only helpful for my specific use-case and other devs may want to access these methods from the proper thread.

In my use-case, I would designate my ValueTree for usage by only the Message Thread. I would connect the ValueTree to UI Components. Then I would create a set of CachedValueReadOnly to .referTo() different ValueTree properties (executing the .referTo() calls on the Message Thread). Then, for the remainder of my program, I would allow a separate thread to only make read calls on the CachedValueReadOnly objects. This way, the separate thread receives ValueTree updates in a thread-safe manner and the developer is blocked from attempting a thread-unsafe write to the CachedValueReadOnly.