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!
