From 0cadf73dc0655a942c61f623e2fe3b691111d6b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Sat, 16 Jun 2018 22:27:27 +0200 Subject: [PATCH] Introduce borrowable_ptr including a unittest a threadsave calback solution --- CMakeLists.txt | 1 + src/test/borrowabletest.cpp | 62 +++++++++++++++ src/util/borrowable_ptr.h | 151 ++++++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 src/test/borrowabletest.cpp create mode 100644 src/util/borrowable_ptr.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c9dfc312e3..058b5a1a654 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2055,6 +2055,7 @@ add_executable(mixxx-test # src/test/signalpathtest.cpp src/test/fifotest.cpp lib/portaudio/pam_ringbuffer.cpp + src/test/borrowabletest.cpp ) if (QML) target_sources(mixxx-test PRIVATE diff --git a/src/test/borrowabletest.cpp b/src/test/borrowabletest.cpp new file mode 100644 index 00000000000..e2164104cf5 --- /dev/null +++ b/src/test/borrowabletest.cpp @@ -0,0 +1,62 @@ +#include +#include + +#include +#include + +namespace { + +class BorrowableTest : public testing::Test { +}; + +TEST_F(BorrowableTest, SingleThread) { + int i = 5; + { + auto borrowable = borrowable_ptr(&i); + { + borrowed_ptr borrowed1 = borrowable.borrow(); + borrowed_ptr borrowed2 = borrowable.borrow(); + } + } +} + +TEST_F(BorrowableTest, TwoThreads) { + int i = 1; + int j = 2; + + auto borrowable = borrowable_ptr(&i); + + auto future1 = QtConcurrent::run([&borrowable]() { + for (int k = 0; k < 10; ++k) { + borrowed_ptr borrowed1 = borrowable.borrow(); + borrowed_ptr borrowed2 = borrowable.borrow(); + int* p1 = borrowed1.get(); + int* p2 = borrowed1.get(); + qDebug() << "future1" << (p1 ? *p1 : 0) << (p2 ? *p2 : 0); + } + }); + + auto future2 = QtConcurrent::run([&borrowable]() { + for (int k = 0; k < 10; ++k) { + borrowed_ptr borrowed1 = borrowable.borrow(); + borrowed_ptr borrowed2 = borrowable.borrow(); + int* p1 = borrowed1.get(); + int* p2 = borrowed1.get(); + qDebug() << "future2" << (p1 ? *p1 : 0) << (p2 ? *p2 : 0); + } + }); + + // waist a bit of time implicit sync on the stdout + for (int i = 0; i < 5; ++i) { + qDebug() << "main"; + } + + // replace borrowable object + borrowable = borrowable_ptr(&j); + + // Wait for both tasks to complete + future1.waitForFinished(); + future2.waitForFinished(); +} + +} // namespace diff --git a/src/util/borrowable_ptr.h b/src/util/borrowable_ptr.h new file mode 100644 index 00000000000..2c6dbdcb2c6 --- /dev/null +++ b/src/util/borrowable_ptr.h @@ -0,0 +1,151 @@ +#pragma once + +#include +#include + +#include "util/assert.h" + +// @brief A wrapper class to protect pointers owned by another entity, allowing +// safe access to the referenced object across threads. This prevents the +// object from being deleted by its owner while other threads are executing +// functions on the referenced object, implemented mostly lock-free, except during +// reset(), if the object is still in use. +// +// Use cases are: +// - Implementing callbacks to objects with shorter lifetimes. +// - A safe replacement for Qt's direct connections, which must not be used across +// threads in such cases. +// +// @note Unlike `std::shared_ptr`, the object is not deleted when the last +// `borrowed_ptr` falls out of scope. Ownership and deletion are always managed +// by the original owner (via `borrowable_ptr`). +// +// If the `borrowable_ptr` falls out of scope while there are active `borrowed_ptr` +// instances, the thread is suspended until all `borrowed_ptr` instances are deleted +// and the reference count reaches zero. +// +// Thread safety: +// - It is thread-safe to replace the object in `borrowable_ptr`. +// - The transition is not atomic: `borrowed_ptr` instances become null before pointing +// to the new object. + +// Usage: +// int i = 5; +// { +// // Make i borrowable, owned by the stack +// auto borrowable = borrowable_ptr(&i); +// { +// // Borrow i, this can be borrowd across threads +// // borrowed_ptr is a strong reference that guranties the borrowed object is +// // valid unit the end of the scope +// borrowed_ptr borrowed1 = borrowable.borrow(); +// borrowed_ptr borrowed2 = borrowable.borrow(); +// } // borrowed objects are returned +// } // borrowable_ptr falls out of scope, suspended if a borrowed pointer is not returned + +template +class borrowable_ptr; + +template +class borrowed_ptr : public std::shared_ptr { + public: + constexpr borrowed_ptr() noexcept + : std::shared_ptr() { + } + + borrowed_ptr(const borrowed_ptr& other) noexcept + : std::shared_ptr(other) { + } + + borrowed_ptr(borrowed_ptr&& other) noexcept + : std::shared_ptr(std::move(other)) { + } + + borrowed_ptr& operator=(const borrowed_ptr& other) noexcept = default; + + private: + // @brief Internal constructor used by `borrowable_ptr` to create a borrowed pointer. + // @param other A non owning shared pointer to the managed object. + borrowed_ptr(const std::shared_ptr& other) noexcept + : std::shared_ptr(other) { + } + + friend class borrowable_ptr; +}; + +template +class borrowable_ptr { + private: + // @brief Custom deleter to manage the mutex during object destruction. + class borrowable_deleter { + public: + explicit borrowable_deleter(QMutex* pMutex) + : m_pMutex(pMutex) { + // Lock the mutex to indicate the object is in use. + m_pMutex->lock(); + } + + template + void operator()(T*) { + // Last reference is gone; release the mutex to allow `borrowable_ptr` to clean up. + m_pMutex->unlock(); + } + + private: + QMutex* m_pMutex; + }; + + public: + borrowable_ptr() { + } + + // @brief Construct a `borrowable_ptr` managing a raw pointer but not owning + // @param p Raw pointer to the managed object. + explicit borrowable_ptr(Tp* p) { + if (p) { + m_sharedPtr = std::shared_ptr(p, borrowable_deleter(&m_mutex)); + } + } + + ~borrowable_ptr() { + reset(); + m_mutex.unlock(); + } + + // @brief Assign a new raw pointer to the `borrowable_ptr` but not owning + // @param p Raw pointer to the new object. + // @return Reference to this instance. + borrowable_ptr& operator=(Tp* p) { + reset(); + m_mutex.unlock(); + m_sharedPtr = std::shared_ptr(p, borrowable_deleter(&m_mutex)); + return *this; + } + + // @brief Borrow a strong reference to the managed object. + // @return A `borrowed_ptr` instance pointing to the managed object. + borrowed_ptr borrow() { + return borrowed_ptr(m_sharedPtr); + } + + borrowable_ptr& operator=(const borrowable_ptr& other) { + this->operator=(other.get()); + return *this; + } + + void reset() { + m_sharedPtr.reset(); + // Wait until all borrowed references are released. + m_mutex.lock(); + } + + // @brief Get the raw pointer to the managed object. + // @return Raw pointer to the managed object. + Tp* get() const noexcept { + return m_sharedPtr.get(); + } + + private: + QMutex m_mutex; + std::shared_ptr m_sharedPtr; ///< Non-owning shared pointer to the managed object. +};