Using Thread to create new class instance?

A bit of a c++ noob, so please bare with me and feel free to school me if I say something stupid/word things in a confusing way.

My plugin involves an Instructions class that tells processBlock() what to do with incoming samples. This Instructions object has the potential to be quite large (up to ~0.5 MB), and takes a few milliseconds to compute. The user has the ability to change the parameters of the Instructions, which requires the Instruction to be re-constructed and replaced. Right now, changing these parameters and re-calculating the Instructions during playback can cause the plugin to lag.

What I want to do is implement a juce::Thread that can be called to calculate a new Instructions instance. My idea is to have this Thread object hold a NewInstructions member class instance, and when this NewInstructions is complete, replace the Instructions instance in the PluginProcessor at the beginning of processBlock(). In the meantime, while being re-calculated, I want processBlock() to simply use the old Instructions instance.

My question is: is this a reasonable thing to do, or am I completely off base? Also, if the user changes the Instructions parameters while NewInstructions are being calculated, can I safely kill the Thread and call it again with the new parameters? Obviously, I will be careful about thread safety.

Thanks in advance for your insight/help. I am amazed by how helpful this community has been for me!

That sounds pretty sensible. There can be a few gotchas on the way though.

  • Make sure that if you use reference counted memory handling like std::shared_ptr and friends the objects aren’t deleted on the real-time thread.
  • You can safely kill threads … make sure to check Thread::threadShouldExit() regularly while preforming the calculations and call Thread::waitForThreadToExit() before deleting the thread.

Depending on how regularly these calculations need to happen ThreadPools might be an alternative approach.

I would advise against involving another thread. If generating those instructions is fast enough not to stall the UI, there is no need for another thread. But I would need to know, what is constructing those instructions.
The setup has a fair chance for priority inversion, i.e. the audio thread waiting to receive said instruction object. You will need a lockfree FIFO to supply the instruction.

I would point you to the great talk from @dave96 and @fabian, if you have some spare time to watch, it is more than worth it (both parts, actually):

The rewritten dsp::Convolution class in JUCE 6 does something quite similar to this. When a user requests to load an impulse response, we hand that work off to a background thread which constructs a new convolution engine. At the top of the Convolution’s process function, we check whether there’s a new pending engine, and swap the engines over if so. There’s also some machinery to crossfade between the old and new engines, so that there aren’t any pops/clicks when loading new IRs.

To get commands from the audio thread onto the background thread, we use a JUCE AbstractFifo, and to get the newly-built engines from the background thread onto the audio thread we use a unique_ptr protected by a try-locked SpinLock.

I’ve had a similar kind of thing, where the audio thread produced an object that the message thread read later. It was a different because the object was both produced and consumed continuously -I drop it just in case. What I did was having a buffer of three objects, two normal pointers, an atomic pointer, and a flag.

Object buffer[3];
Object* write{ buffer + 1 }, * read{ buffer + 2 };
std::atomic<Object*> inter{ buffer };
std::atomic_bool pending{};

The producer writes through the write pointer. When it finishes:

write = inter.exchange (write);
pending = true;

The consumer reads through the read pointer. At the start of each reading round:

if (pending.exchange (false))
    read = inter.exchange (read);

It’s basically triple buffering, or a very short fifo.

Thank you all for your replies. I got everything working and it was actually pretty straightforward in my case; since I only ever really need one thread running at a time, it was easy enough to ensure thread-safety by using Thread::isThreadRunning() and only copying over the new instructions if this returns false, as well as a restartThread boolean on the processor side to indicate if the user had again changed the parameters while new instructions were being generated, and to start over again if this were the case. @reuk thanks for mentioning crossfading as it seems I will need to do something similar.