From 3c3edf6f6de35994f6617f02d9ed29587d826f84 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Sat, 9 Nov 2024 23:55:57 -0500 Subject: [PATCH] Add ThreadSafeSingleton. Easy singleton pattern with thread-safe guard. --- lib/core/CMakeLists.txt | 2 + .../res/snippets/qx-threadsafesingleton.cpp | 33 +++++++++++ lib/core/include/qx/core/qx-exclusiveaccess.h | 2 +- .../include/qx/core/qx-threadsafesingleton.h | 44 ++++++++++++++ lib/core/src/qx-exclusiveaccess.dox | 32 +++++----- lib/core/src/qx-threadsafesingleton.dox | 58 +++++++++++++++++++ 6 files changed, 154 insertions(+), 17 deletions(-) create mode 100644 lib/core/doc/res/snippets/qx-threadsafesingleton.cpp create mode 100644 lib/core/include/qx/core/qx-threadsafesingleton.h create mode 100644 lib/core/src/qx-threadsafesingleton.dox diff --git a/lib/core/CMakeLists.txt b/lib/core/CMakeLists.txt index 772f0543..61cd27e5 100644 --- a/lib/core/CMakeLists.txt +++ b/lib/core/CMakeLists.txt @@ -26,6 +26,7 @@ qx_add_component("Core" qx-processbider.h qx-progressgroup.h qx-table.h + qx-threadsafesingleton.h qx-versionnumber.h qx-regularexpression.h qx-setonce.h @@ -87,6 +88,7 @@ qx_add_component("Core" qx-array.dox qx-setonce.dox qx-table.dox + qx-threadsafesingleton.dox LINKS PUBLIC ${Qt}::Core diff --git a/lib/core/doc/res/snippets/qx-threadsafesingleton.cpp b/lib/core/doc/res/snippets/qx-threadsafesingleton.cpp new file mode 100644 index 00000000..6bc27ee2 --- /dev/null +++ b/lib/core/doc/res/snippets/qx-threadsafesingleton.cpp @@ -0,0 +1,33 @@ +//! [0] +class MySingleton : Qx::ThreadSafeSingleton +{ + QX_THREAD_SAFE_SINGLETON(MySingleton); +private: + std::string mData; + MySingleton() = default; // Generally should be private + +public: + doStuffSafely() { mData = "I'm for sure set while not being read!"; } + checkStuffSafely() { return mData; // Not being written to when returned } +} + +//... + +void functionInArbitraryThread() +{ + auto singleton = MySingleton::instance(); + // This function now has a exclusive access to MySingleton (i.e. a mutex lock is established) + singleton->doStuffSafely(); + + // Unlocked when 'singleton' goes out of scope, or is manually unlocked. +} + +void functionInAnotherThread() +{ + // Safely lock and read. It's guarenteed that no other thread is using MySingleton + // after the instance is obtained. + auto singleton = MySingleton::instance(); + std::string info = singleton->checkStuffSafely(); + //... +} +//! [0] diff --git a/lib/core/include/qx/core/qx-exclusiveaccess.h b/lib/core/include/qx/core/qx-exclusiveaccess.h index 6839f847..105c85dc 100644 --- a/lib/core/include/qx/core/qx-exclusiveaccess.h +++ b/lib/core/include/qx/core/qx-exclusiveaccess.h @@ -10,7 +10,7 @@ class QRecursiveMutex; namespace Qx { -template +template requires any_of class ExclusiveAccess { diff --git a/lib/core/include/qx/core/qx-threadsafesingleton.h b/lib/core/include/qx/core/qx-threadsafesingleton.h new file mode 100644 index 00000000..b043cab2 --- /dev/null +++ b/lib/core/include/qx/core/qx-threadsafesingleton.h @@ -0,0 +1,44 @@ +#ifndef QX_THREADSAFE_SINGLETON +#define QX_THREADSAFE_SINGLETON + +// Intra-component Includes +#include "qx/core/qx-exclusiveaccess.h" + +// Extra-component Includes +#include "qx/utility/qx-concepts.h" + +class QMutex; +class QRecursiveMutex; + +namespace Qx +{ + +template + requires any_of +class ThreadSafeSingleton +{ +//-Class Members--------------------------------------------------------------------------------------------- +private: + // Needs to be static so it can be locked before the the singleton is created, or else a race in instance() could occur. + static inline constinit Mutex smMutex; + +//-Constructor---------------------------------------------------------------------------------------------- +protected: + ThreadSafeSingleton() = default; + +//-Class Functions---------------------------------------------------------------------------------------------- +public: + static Qx::ExclusiveAccess instance() + { + static Singleton s; + return Qx::ExclusiveAccess(&s, &smMutex); // Provides locked access to singleton, that unlocks when destroyed + } +}; + +//-Macros---------------------------------------------------------------------------------------------------------- +// Macro to be used in all derivatives +#define QX_THREAD_SAFE_SINGLETON(Singleton) friend ThreadSafeSingleton + +} + +#endif // QX_THREADSAFE_SINGLETON diff --git a/lib/core/src/qx-exclusiveaccess.dox b/lib/core/src/qx-exclusiveaccess.dox index be1533df..c55d71a0 100644 --- a/lib/core/src/qx-exclusiveaccess.dox +++ b/lib/core/src/qx-exclusiveaccess.dox @@ -23,7 +23,7 @@ namespace Qx //-Constructor---------------------------------------------------------------------------------------------- //Public: /*! - * @fn ExclusiveAccess::ExclusiveAccess(AccessType* data, Mutex* mutex) + * @fn ExclusiveAccess::ExclusiveAccess(AccessType* data, Mutex* mutex) * * Constructs an ExclusiveAccess and locks @a mutex. The mutex will be unlocked when the ExclusiveAccess is * destroyed. If @a mutex is @c nullptr, ExclusiveAccess only provides access to @a data. @@ -32,7 +32,7 @@ namespace Qx */ /*! - * @fn ExclusiveAccess::ExclusiveAccess(ExclusiveAccess&& other) + * @fn ExclusiveAccess::ExclusiveAccess(ExclusiveAccess&& other) * * Move-constructs and ExclusiveAccess from @a other. The mutex, data pointer, and state of @a other is * transferred to the newly constructed instance. After the move, @a other will no longer manage the mutex, @@ -44,28 +44,28 @@ namespace Qx //-Destructor------------------------------------------------------------------------------------------------ //Public: /*! - * @fn ExclusiveAccess::~ExclusiveAccess() + * @fn ExclusiveAccess::~ExclusiveAccess() * * Destroys the ExclusiveAccess and unlocks the mutex provided by the constructor if it's still locked. */ -//-Class Functions---------------------------------------------------------------------------------------------- +//-Instance Functions---------------------------------------------------------------------------------------------- //Public: /*! - * @fn void ExclusiveAccess::isLocked() const + * @fn void ExclusiveAccess::isLocked() const * * Returns @c true if this ExclusiveAccess is currently locking its associated mutex; otherwise, returns * @c false. */ /*! - * @fn void ExclusiveAccess::mutex() const + * @fn void ExclusiveAccess::mutex() const * * Returns the mutex on which the ExclusiveAccess is operating. */ /*! - * @fn void ExclusiveAccess::relock() + * @fn void ExclusiveAccess::relock() * * Relocks an unlocked ExclusiveAccess. * @@ -73,14 +73,14 @@ namespace Qx */ /*! - * @fn void ExclusiveAccess::swap(ExclusiveAccess& other) + * @fn void ExclusiveAccess::swap(ExclusiveAccess& other) * * Swaps the mutex, data pointer, and state of this ExclusiveAccess with @a other. This operation * is very fast and never fails. */ /*! -* @fn void ExclusiveAccess::unlock() +* @fn void ExclusiveAccess::unlock() * * Unlocks this ExclusiveAccess. You can use relock() to lock it again. It does not need to be * locked when destroyed. @@ -89,43 +89,43 @@ namespace Qx */ /*! -* @fn AccessType* ExclusiveAccess::access() +* @fn AccessType* ExclusiveAccess::access() * * Returns a pointer to the data the ExclusiveAccess is providing access to. */ /*! -* @fn const AccessType* ExclusiveAccess::access() const +* @fn const AccessType* ExclusiveAccess::access() const * * @overload */ /*! -* @fn AccessType& ExclusiveAccess::operator*() +* @fn AccessType& ExclusiveAccess::operator*() * * Returns a reference to the data the ExclusiveAccess is providing access to. */ /*! -* @fn const AccessType& ExclusiveAccess::operator*() const +* @fn const AccessType& ExclusiveAccess::operator*() const * * @overload */ /*! -* @fn AccessType* ExclusiveAccess::operator->() +* @fn AccessType* ExclusiveAccess::operator->() * * Provides convenient access to the members of @a DataType for the accessible data. */ /*! -* @fn const AccessType* ExclusiveAccess::operator->() const +* @fn const AccessType* ExclusiveAccess::operator->() const * * @overload */ /*! -* @fn void ExclusiveAccess::operator=(ExclusiveAccess&& other) +* @fn void ExclusiveAccess::operator=(ExclusiveAccess&& other) * * Move-assigns @a other onto this ExclusiveAccess. If this ExclusiveAccess was holding onto a * locked mutex before the assignment, the mutex will be unlocked. The mutex, data pointer, and diff --git a/lib/core/src/qx-threadsafesingleton.dox b/lib/core/src/qx-threadsafesingleton.dox new file mode 100644 index 00000000..de3999e9 --- /dev/null +++ b/lib/core/src/qx-threadsafesingleton.dox @@ -0,0 +1,58 @@ +namespace Qx +{ +//=============================================================================================================== +// ThreadSafeSingleton +//=============================================================================================================== + +/*! + * @class ThreadSafeSingleton qx/core/qx-threadsafesingleton.h + * @ingroup qx-core + * + * @brief The ThreadSafeSingleton template class provides access to a static singleton instance in a thread-safe + * manner. + * + * This class allows one to easily utilize the singleton pattern while ensuring that access to the global shared + * instance remains thread-safe. This is achieved by providing basic scaffolding through a base class from which + * the actual singleton should derive. The instance() method provides a mutex protected pointer to the + * singleton instance via Qx::ExclusiveAccess. By declaring the final class' constructor as private, and accessing + * the class through instance(), one can be certain that the instance is only ever being used by one thread at a + * time. + * + * If code in your singleton calls external code that in turn results in instance() being called again + * from within the same thread, be sure to use QRecursiveMutex when instantiating this template or else you + * may cause a dead-lock. + * + * The following is a complete example for using Qx::ThreadSafeSingleton: + * + * @snippet qx-threadsafesingleton.cpp 0 + */ + +//-Constructor---------------------------------------------------------------------------------------------- +//Protected: +/*! + * @fn ThreadSafeSingleton::ThreadSafeSingleton() + * + * Constructs a ThreadSafeSingleton. + * + * @sa instance(). + */ + +//-Class Functions---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn Qx::ExclusiveAccess ThreadSafeSingleton::instance() + * + * Returns a handle to the singleton instance. + */ + +//-Macros---------------------------------------------------------------------------------------------------------- +/*! + * @def QX_THREAD_SAFE_SINGLETON(Singleton) + * + * This macro must be used within the class definition of any singleton that derived from this class, similar to + * Q_OBJECT for classes derived from QObject. + * + * The argument is to be the name of the derived class itself (similar to CRTP). + */ + +}