Skip to content

Commit

Permalink
Introduce borrowable_ptr including a unittest a threadsave calback so…
Browse files Browse the repository at this point in the history
…lution
  • Loading branch information
daschuer committed Dec 9, 2024
1 parent 3b14af5 commit 0cadf73
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 0 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions src/test/borrowabletest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#include <gtest/gtest.h>
#include <util/borrowable_ptr.h>

#include <QtConcurrent>
#include <QtDebug>

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
151 changes: 151 additions & 0 deletions src/util/borrowable_ptr.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#pragma once

#include <QMutex>
#include <memory>

#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<typename Tp>
class borrowable_ptr;

template<typename Tp>
class borrowed_ptr : public std::shared_ptr<Tp> {
public:
constexpr borrowed_ptr() noexcept
: std::shared_ptr<Tp>() {
}

borrowed_ptr(const borrowed_ptr<Tp>& other) noexcept
: std::shared_ptr<Tp>(other) {
}

borrowed_ptr(borrowed_ptr<Tp>&& other) noexcept
: std::shared_ptr<Tp>(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<Tp>& other) noexcept
: std::shared_ptr<Tp>(other) {
}

friend class borrowable_ptr<Tp>;
};

template<typename Tp>
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<typename T>
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<Tp>(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<Tp>(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<Tp> borrow() {
return borrowed_ptr<Tp>(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<Tp> m_sharedPtr; ///< Non-owning shared pointer to the managed object.
};

0 comments on commit 0cadf73

Please sign in to comment.