From 8fc76c4376e3fbfeaa9839ef68e97dd9626674e7 Mon Sep 17 00:00:00 2001 From: reuk Date: Thu, 13 Apr 2023 18:00:15 +0100 Subject: [PATCH] LockingAsyncUpdater: Add a new slightly-more-threadsafe AsyncUpdater alternative --- .../broadcasters/juce_LockingAsyncUpdater.cpp | 133 ++++++++++++++++++ .../broadcasters/juce_LockingAsyncUpdater.h | 113 +++++++++++++++ modules/juce_events/juce_events.cpp | 1 + modules/juce_events/juce_events.h | 1 + 4 files changed, 248 insertions(+) create mode 100644 modules/juce_events/broadcasters/juce_LockingAsyncUpdater.cpp create mode 100644 modules/juce_events/broadcasters/juce_LockingAsyncUpdater.h diff --git a/modules/juce_events/broadcasters/juce_LockingAsyncUpdater.cpp b/modules/juce_events/broadcasters/juce_LockingAsyncUpdater.cpp new file mode 100644 index 0000000000..c87db1a595 --- /dev/null +++ b/modules/juce_events/broadcasters/juce_LockingAsyncUpdater.cpp @@ -0,0 +1,133 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +class LockingAsyncUpdater::Impl : public CallbackMessage +{ +public: + explicit Impl (std::function cb) + : callback (std::move (cb)) {} + + void clear() + { + const ScopedLock lock (mutex); + deliver = false; + callback = nullptr; + } + + void trigger() + { + { + const ScopedLock lock (mutex); + + if (deliver) + return; + + deliver = true; + } + + if (! post()) + cancel(); + } + + void cancel() + { + const ScopedLock lock (mutex); + deliver = false; + } + + bool isPending() + { + const ScopedLock lock (mutex); + return deliver; + } + + void messageCallback() override + { + const ScopedLock lock (mutex); + + if (std::exchange (deliver, false)) + NullCheckedInvocation::invoke (callback); + } + +private: + CriticalSection mutex; + std::function callback; + bool deliver = false; +}; + +//============================================================================== +LockingAsyncUpdater::LockingAsyncUpdater (std::function callbackToUse) + : impl (new Impl (std::move (callbackToUse))) {} + +LockingAsyncUpdater::LockingAsyncUpdater (LockingAsyncUpdater&& other) noexcept + : impl (std::exchange (other.impl, nullptr)) {} + +LockingAsyncUpdater& LockingAsyncUpdater::operator= (LockingAsyncUpdater&& other) noexcept +{ + LockingAsyncUpdater temp { std::move (other) }; + std::swap (temp.impl, impl); + return *this; +} + +LockingAsyncUpdater::~LockingAsyncUpdater() +{ + if (impl != nullptr) + impl->clear(); +} + +void LockingAsyncUpdater::triggerAsyncUpdate() +{ + if (impl != nullptr) + impl->trigger(); + else + jassertfalse; // moved-from! +} + +void LockingAsyncUpdater::cancelPendingUpdate() noexcept +{ + if (impl != nullptr) + impl->cancel(); + else + jassertfalse; // moved-from! +} + +void LockingAsyncUpdater::handleUpdateNowIfNeeded() +{ + if (impl != nullptr) + impl->messageCallback(); + else + jassertfalse; // moved-from! +} + +bool LockingAsyncUpdater::isUpdatePending() const noexcept +{ + if (impl != nullptr) + return impl->isPending(); + + jassertfalse; // moved-from! + return false; +} + +} // namespace juce diff --git a/modules/juce_events/broadcasters/juce_LockingAsyncUpdater.h b/modules/juce_events/broadcasters/juce_LockingAsyncUpdater.h new file mode 100644 index 0000000000..6a9e449d8b --- /dev/null +++ b/modules/juce_events/broadcasters/juce_LockingAsyncUpdater.h @@ -0,0 +1,113 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + A bit like an AsyncUpdater, but guarantees that after cancelPendingUpdate() returns, + the async function will never be called until triggerAsyncUpdate() is called again. + This is an important guarantee for writing classes with async behaviour that can + still be destroyed safely from a background thread. + + Note that all of the member functions of this type have a chance of blocking, so + this class is unsuitable for broadcasting changes from a realtime thread. + + @tags{Events} +*/ +class JUCE_API LockingAsyncUpdater final +{ +public: + //============================================================================== + /** Creates a LockingAsyncUpdater object that will call the provided callback + on the main thread when triggered. + + Note that the LockingAsyncUpdater takes an internal mutex before calling + the provided callback. Therefore, in order to avoid deadlocks, you should + (ideally) ensure that no locks are taken inside the callbackToUse. If you + do need to take a lock inside the callback, make sure that you do not + hold the same lock while calling any of the LockingAsyncUpdater member + functions. + */ + explicit LockingAsyncUpdater (std::function callbackToUse); + + /** Move constructor. */ + LockingAsyncUpdater (LockingAsyncUpdater&& other) noexcept; + + /** Move assignment operator. */ + LockingAsyncUpdater& operator= (LockingAsyncUpdater&& other) noexcept; + + /** Destructor. + If there are any pending callbacks when the object is deleted, these are lost. + The async callback is guaranteed not to be called again once the destructor has + completed. + */ + ~LockingAsyncUpdater(); + + //============================================================================== + /** Causes the callback to be triggered at a later time. + + This method returns quickly, after which a callback to the + handleAsyncUpdate() method will be made by the impl thread as + soon as possible. + + If an update callback is already pending but hasn't started yet, calling + this method will have no effect. + + It's thread-safe to call this method from any thread, BUT beware of calling + it from a real-time (e.g. audio) thread, because it unconditionally locks + a mutex. This may block, e.g. if this is called from a background thread + while the async callback is in progress on the main thread. + */ + void triggerAsyncUpdate(); + + /** This will stop any pending updates from happening. + + If a callback is already in progress on another thread, this will block until + the callback has finished before returning. + */ + void cancelPendingUpdate() noexcept; + + /** If an update has been triggered and is pending, this will invoke it + synchronously. + + Use this as a kind of "flush" operation - if an update is pending, the + handleAsyncUpdate() method will be called immediately; if no update is + pending, then nothing will be done. + + Because this may invoke the callback, this method must only be called on + the main event thread. + */ + void handleUpdateNowIfNeeded(); + + /** Returns true if there's an update callback in the pipeline. */ + bool isUpdatePending() const noexcept; + +private: + class Impl; + ReferenceCountedObjectPtr impl; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LockingAsyncUpdater) +}; + +} // namespace juce diff --git a/modules/juce_events/juce_events.cpp b/modules/juce_events/juce_events.cpp index c357c70de5..67d555a942 100644 --- a/modules/juce_events/juce_events.cpp +++ b/modules/juce_events/juce_events.cpp @@ -60,6 +60,7 @@ #include "messages/juce_MessageManager.cpp" #include "broadcasters/juce_ActionBroadcaster.cpp" #include "broadcasters/juce_AsyncUpdater.cpp" +#include "broadcasters/juce_LockingAsyncUpdater.cpp" #include "broadcasters/juce_ChangeBroadcaster.cpp" #include "timers/juce_MultiTimer.cpp" #include "timers/juce_Timer.cpp" diff --git a/modules/juce_events/juce_events.h b/modules/juce_events/juce_events.h index dd6a79ecfa..67afe05cf2 100644 --- a/modules/juce_events/juce_events.h +++ b/modules/juce_events/juce_events.h @@ -82,6 +82,7 @@ #include "broadcasters/juce_ActionBroadcaster.h" #include "broadcasters/juce_ActionListener.h" #include "broadcasters/juce_AsyncUpdater.h" +#include "broadcasters/juce_LockingAsyncUpdater.h" #include "broadcasters/juce_ChangeListener.h" #include "broadcasters/juce_ChangeBroadcaster.h" #include "timers/juce_Timer.h"