| name | juce-threading |
| description | JUCE threading model and component lifecycle safety. Use when writing UI components, async callbacks, timers, or any code that crosses thread boundaries. Covers destruction ordering, SafePointer, LookAndFeel cleanup, and common crash patterns. |
JUCE Threading Model & Component Lifecycle
This skill covers thread safety and component lifecycle patterns critical for avoiding crashes in MAGDA.
Threading Model
Three Thread Contexts
MessageThread (UI) — Component::paint, resized, mouse events, Timer callbacks, callAsync lambdas
Audio Thread (RT) — processBlock, no allocations, no locks, no MessageManager calls
Background Threads — juce::Thread, juce::ThreadPool, long-running tasks
Check Which Thread You're On
jassert(juce::MessageManager::getInstance()->isThisTheMessageThread());
if (juce::MessageManager::getInstance()->isThisTheMessageThread())
component.repaint();
Cross-Thread Communication
juce::MessageManager::callAsync([this] {
label.setText("Done", juce::dontSendNotification);
});
struct MyComp : juce::Component, juce::AsyncUpdater {
void audioCallback() {
latestValue.store(newVal);
triggerAsyncUpdate();
}
void handleAsyncUpdate() override {
slider.setValue(latestValue.load());
}
std::atomic<float> latestValue { 0.0f };
};
struct MyComp : juce::Component, juce::Timer {
MyComp() { startTimerHz(30); }
~MyComp() override { stopTimer(); }
void timerCallback() override {
repaint();
}
};
juce::MessageManager::callAsync([] {
juce::Timer::callAfterDelay(500, [] {
});
});
Audio Thread Rules
void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi) override
{
}
Component Lifecycle & Destruction Safety
Destruction Ordering
struct ParentComp : juce::Component {
~ParentComp() override
{
stopTimer();
removeListener(this);
}
ChildComp child;
};
Rule: Stop timers and remove listeners in the destructor body, before members are destroyed.
SafePointer for Async Safety
juce::MessageManager::callAsync([this] {
setText("done");
});
juce::MessageManager::callAsync([safeThis = juce::Component::SafePointer<MyComp>(this)] {
if (auto* self = safeThis.getComponent())
self->setText("done");
});
auto safeThis = juce::Component::SafePointer<MyComp>(this);
juce::MessageManager::callAsync([safeThis]() mutable {
if (safeThis != nullptr)
safeThis->setText("done");
});
Timer Safety
struct MyComp : juce::Component, juce::Timer {
MyComp() { startTimerHz(30); }
~MyComp() override {
stopTimer();
}
void timerCallback() override {
repaint();
}
};
LookAndFeel Cleanup
struct MyApp {
CustomLookAndFeel lnf;
std::unique_ptr<MainWindow> mainWindow;
};
struct MyApp {
~MyApp() {
juce::LookAndFeel::setDefaultLookAndFeel(nullptr);
mainWindow.reset();
}
CustomLookAndFeel lnf;
std::unique_ptr<MainWindow> mainWindow;
};
struct MyComp : juce::Component {
~MyComp() override {
setLookAndFeel(nullptr);
}
};
Listener Removal
struct MyComp : juce::Component, juce::Value::Listener {
MyComp(juce::Value& v) : value(v) {
value.addListener(this);
}
~MyComp() override {
value.removeListener(this);
}
void valueChanged(juce::Value&) override {
repaint();
}
juce::Value value;
};
JUCE Leak Detector
class MyComponent : public juce::Component {
public:
MyComponent();
~MyComponent() override;
private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MyComponent)
};
Common Crash Patterns & Fixes
1. Timer fires after component deleted
struct Bad : juce::Component, juce::Timer {
~Bad() override { }
};
struct Good : juce::Component, juce::Timer {
~Good() override { stopTimer(); }
};
2. callAsync lambda captures dead this
juce::MessageManager::callAsync([this] { repaint(); });
juce::MessageManager::callAsync(
[safe = juce::Component::SafePointer<MyComp>(this)] {
if (safe != nullptr) safe->repaint();
});
3. LookAndFeel destroyed before components
struct App {
CustomLookAndFeel lnf;
std::unique_ptr<MainWindow> window;
};
struct App {
~App() {
juce::LookAndFeel::setDefaultLookAndFeel(nullptr);
window.reset();
}
CustomLookAndFeel lnf;
std::unique_ptr<MainWindow> window;
};
4. Listener not removed before object destroyed
struct Bad : juce::Button::Listener {
Bad(juce::Button& b) : button(b) { button.addListener(this); }
~Bad() { }
juce::Button& button;
};
struct Good : juce::Button::Listener {
Good(juce::Button& b) : button(b) { button.addListener(this); }
~Good() { button.removeListener(this); }
juce::Button& button;
};
5. Component deleted during its own callback
void buttonClicked(juce::Button*) override {
owner.removeChildComponent(this);
delete this;
}
void buttonClicked(juce::Button*) override {
juce::MessageManager::callAsync([this, &owner = this->owner] {
owner.removeAndDeleteChild(this);
});
}
6. Accessing MessageManager from audio thread
void processBlock(...) {
if (clipping)
component.repaint();
}
void processBlock(...) {
if (clipping)
clipFlag.store(true);
}
void timerCallback() override {
if (clipFlag.exchange(false))
clipIndicator.repaint();
}
Prefer RAII Over Manual Resource Management
Always use RAII classes instead of manual acquire/release patterns. This prevents resource leaks when exceptions occur or early returns are taken.
Smart Pointers Over Raw new/delete
auto* comp = new MyComponent();
addAndMakeVisible(comp);
auto comp = std::make_unique<MyComponent>();
addAndMakeVisible(comp.get());
ownedComponents.push_back(std::move(comp));
Scoped Locks
mutex.lock();
doWork();
if (error) return;
mutex.unlock();
{
const juce::ScopedLock sl(mutex);
doWork();
if (error) return;
}
Tracktion Engine RAII Helpers
{
te::TransportControl::ReallocationInhibitor inhibitor(transport);
}
{
te::TransportControl::ScopedPlaybackRestarter restarter(transport);
transport.stop(false, false);
}
{
te::Edit::UndoTransactionInhibitor inhibitor(*edit);
}
RAII for Listener Registration
struct ScopedListener {
ScopedListener(juce::Value& v, juce::Value::Listener* l)
: value(v), listener(l) { value.addListener(listener); }
~ScopedListener() { value.removeListener(listener); }
juce::Value& value;
juce::Value::Listener* listener;
};
RAII for Timer Management
~MyComp() override { stopTimer(); }
struct ScopedTimer : juce::Timer {
std::function<void()> callback;
~ScopedTimer() override { stopTimer(); }
void timerCallback() override { if (callback) callback(); }
};
Thread-Safe Data Patterns
std::atomic<float> gain { 1.0f };
std::atomic<bool> shouldStop { false };
juce::AbstractFifo fifo { 512 };
std::array<float, 512> buffer;
juce::SpinLock lock;
{
const juce::SpinLock::ScopedLockType sl(lock);
}