Skip to content

Commit

Permalink
feat(libsinsp): Add wrapper for read/write lock
Browse files Browse the repository at this point in the history
`mutex.h` already has a nice abstraction that allows exclusive access to
an object. This change extends that paradigm to allow similar simple
function calls that allow read/write locking primitives using a
`shared_mutex`.
  • Loading branch information
greyhame-s committed May 22, 2024
1 parent 00b4d19 commit 361b970
Showing 1 changed file with 187 additions and 50 deletions.
237 changes: 187 additions & 50 deletions userspace/libsinsp/mutex.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ limitations under the License.
#pragma once

#include <mutex>
#include <shared_mutex>
#include <thread>
#include "utils.h"

namespace libsinsp {
template<typename T>
class ConstMutexGuard;
namespace libsinsp
{
template<typename T> class ConstMutexGuard;

/**
* \brief A wrapper to allow synchronized access to a value owned by a Mutex<T>
Expand All @@ -33,34 +35,24 @@ class ConstMutexGuard;
* It works by simply holding a `std::unique_lock` object that keeps the mutex
* locked while it exists and unlocks it upon destruction
*/
template<typename T>
class MutexGuard {
template<typename T> class MutexGuard
{
public:
MutexGuard(std::unique_lock<std::mutex> lock, T *inner) : m_lock(std::move(lock)), m_inner(inner) {}
MutexGuard(std::unique_lock<std::mutex> lock, T *inner): m_lock(std::move(lock)), m_inner(inner) {}

// we cannot copy a MutexGuard, only move
MutexGuard(MutexGuard &rhs) = delete;
MutexGuard& operator=(MutexGuard &rhs) = delete;
MutexGuard(MutexGuard &&rhs) noexcept : m_lock(std::move(rhs.m_lock)),
m_inner(rhs.m_inner) {}
MutexGuard &operator=(MutexGuard &rhs) = delete;
MutexGuard(MutexGuard &&rhs) noexcept: m_lock(std::move(rhs.m_lock)), m_inner(rhs.m_inner) {}

T *operator->()
{
return m_inner;
}
T *operator->() { return m_inner; }

T &operator*()
{
return *m_inner;
}
T &operator*() { return *m_inner; }

/**
* Validate that the guarded object exists.
*/
bool valid()
{
return m_inner != nullptr;
}
bool valid() { return m_inner != nullptr; }

private:
std::unique_lock<std::mutex> m_lock;
Expand All @@ -77,44 +69,110 @@ class MutexGuard {
* It works by simply holding a `std::unique_lock` object that keeps the mutex
* locked while it exists and unlocks it upon destruction
*/
template<typename T>
class ConstMutexGuard {
template<typename T> class ConstMutexGuard
{
public:
ConstMutexGuard(std::unique_lock<std::mutex> lock, const T *inner) : m_lock(std::move(lock)),
m_inner(inner) {
}
ConstMutexGuard(std::unique_lock<std::mutex> lock, const T *inner): m_lock(std::move(lock)), m_inner(inner) {}

// we cannot copy a ConstMutexGuard, only move
ConstMutexGuard(ConstMutexGuard &rhs) = delete;
ConstMutexGuard& operator=(ConstMutexGuard &rhs) = delete;
ConstMutexGuard(ConstMutexGuard &&rhs) noexcept : m_lock(std::move(rhs.m_lock)),
m_inner(rhs.m_inner) {}
ConstMutexGuard &operator=(ConstMutexGuard &rhs) = delete;
ConstMutexGuard(ConstMutexGuard &&rhs) noexcept: m_lock(std::move(rhs.m_lock)), m_inner(rhs.m_inner) {}

// a writable guard can be demoted to a read-only one, but *not* the other way around
ConstMutexGuard(MutexGuard<T> &&rhs) noexcept : m_lock(std::move(rhs.m_lock)),
m_inner(rhs.m_inner) // NOLINT(google-explicit-constructor)
{}
ConstMutexGuard(MutexGuard<T> &&rhs) noexcept:
m_lock(std::move(rhs.m_lock)), m_inner(rhs.m_inner) // NOLINT(google-explicit-constructor)
{
}

const T *operator->() const { return m_inner; }

const T &operator*() const { return *m_inner; }

/**
* Validate that the guarded object exists.
*/
bool valid() { return m_inner != nullptr; }

private:
std::unique_lock<std::mutex> m_lock;
const T *m_inner;
};

const T *operator->() const
/**
* \brief A wrapper to allow synchronized write access to a value owned by a SharedMutex<T>,
* while allowing concurrent read access to the value.
*
* @tparam T type of the value protected by the mutex
*
* It works by simply holding a `std::unique_lock` object that keeps the shared_mutex
* exclusively locked while it exists, and unlocks it upon destruction
*/
template<typename T> class SharedMutexGuard
{
public:
SharedMutexGuard(std::unique_lock<std::shared_mutex> wlock, T *inner):
m_write_lock(std::move(wlock)), m_inner(inner)
{
return m_inner;
}

const T &operator*() const
// we cannot copy a SharedMutexGuard, only move
SharedMutexGuard(SharedMutexGuard &rhs) = delete;
SharedMutexGuard &operator=(SharedMutexGuard &rhs) = delete;
SharedMutexGuard(SharedMutexGuard &&rhs) noexcept:
m_write_lock(std::move(rhs.m_write_lock)), m_inner(rhs.m_inner)
{
return *m_inner;
}

T *operator->() { return m_inner; }

T &operator*() { return *m_inner; }

/**
* Validate that the guarded object exists.
*/
bool valid()
bool valid() const { return m_inner != nullptr; }

private:
std::unique_lock<std::shared_mutex> m_write_lock;
T *m_inner;
};

/**
* \brief A wrapper to allow synchronized const read access to a value owned by a SharedMutex<T>
*
* @tparam T type of the value protected by the mutex
*
* It works by simply holding a `std::shared_lock` object that keeps the shared_mutex
* read locked while it exists, and unlocks it upon destruction
*/
template<typename T> class ConstSharedMutexGuard
{
public:
ConstSharedMutexGuard(std::shared_lock<std::shared_mutex> rlock, const T *inner):
m_read_lock(std::move(rlock)), m_inner(inner)
{
}

// we cannot copy a ConstSharedMutexGuard, only move
ConstSharedMutexGuard(ConstSharedMutexGuard &rhs) = delete;
ConstSharedMutexGuard &operator=(ConstSharedMutexGuard &rhs) = delete;
ConstSharedMutexGuard(ConstSharedMutexGuard &&rhs) noexcept:
m_read_lock(std::move(rhs.m_read_lock)), m_inner(rhs.m_inner)
{
return m_inner != nullptr;
}

const T *operator->() const { return m_inner; }

const T &operator*() const { return *m_inner; }

/**
* Validate that the guarded object exists.
*/
bool valid() const { return m_inner != nullptr; }

private:
std::unique_lock<std::mutex> m_lock;
std::shared_lock<std::shared_mutex> m_read_lock = {};
const T *m_inner;
};

Expand All @@ -141,12 +199,12 @@ class ConstMutexGuard {
* size_t num_elts = locked->size();
*
*/
template<typename T>
class Mutex {
template<typename T> class Mutex
{
public:
Mutex() = default;

Mutex(T inner) : m_inner(std::move(inner)) {}
Mutex(T inner): m_inner(std::move(inner)) {}

/**
* \brief Lock the mutex, allowing access to the stored object
Expand All @@ -155,10 +213,7 @@ class Mutex {
* via operator * or -> and ensures the lock is held as long as
* the guard object exists
*/
MutexGuard<T> lock()
{
return MutexGuard<T>(std::unique_lock<std::mutex>(m_lock), &m_inner);
}
MutexGuard<T> lock() { return MutexGuard<T>(std::unique_lock<std::mutex>(m_lock), &m_inner); }

/**
* \brief Lock the mutex, allowing access to the stored object
Expand All @@ -169,13 +224,95 @@ class Mutex {
*
* `const Mutex<T>` only allows read-only access to the protected object
*/
ConstMutexGuard<T> lock() const
ConstMutexGuard<T> lock() const { return ConstMutexGuard<T>(std::unique_lock<std::mutex>(m_lock), &m_inner); }

private:
mutable std::mutex m_lock = {};
T m_inner;
};

/**
* \brief Wrap a value of type T, enforcing synchronized access while allowing for simultaneous readers
*
* @tparam T type of the wrapped value
*
* The class owns a value of type T and a shared_mutex. The only way to access the T inside
* is via the read_lock() and write_lock() methods, which return guard objecta that unlock the mutex
* once they falls out of scope
*
* To protect an object with a shared_mutex, declare a variable of type `SharedMutex<T>`, e.g.
*
* SharedMutex<std::unordered_map<int>> m_locked_map;
*
* Then, to exclusively access the variable for writes, call .write_lock() on the SharedMutex object:
*
* SharedMutexGuard<std::unordered_map<int>> locked = m_locked_map.write_lock();
*
* Now you can call the inner object's methods directly on the guard object,
* which behaves like a smart pointer to the inner object:
*
* size_t num_elts = locked->size();
*
* For concurrent read access to the inner object, use .read_lock() call.
*
* ConstSharedMutexGuard<std::unordered_map<int>> locked = m_locked_map.read_lock();
*
*/
template<typename T> class SharedMutex
{
public:
using time_type = decltype(sinsp_utils::get_current_time_ns());
SharedMutex() = default;

SharedMutex(T inner): m_inner(std::move(inner)) {}

/**
* \brief Lock the mutex, allowing read access to the stored object
*
* The returned guard object allows access to the protected data
* via operator * or -> and ensures the lock is held as long as
* the guard object exists
*
* `ConstSharedMutexGuard<T>` only allows read-only access to the protected object
*/
ConstSharedMutexGuard<T> read_lock() const
{
return ConstMutexGuard<T>(std::unique_lock<std::mutex>(m_lock), &m_inner);
auto start_ns = sinsp_utils::get_current_time_ns();
auto mux_guard = ConstSharedMutexGuard<T>(std::shared_lock<std::shared_mutex>(m_shared_lock), &m_inner);
auto end_ns = sinsp_utils::get_current_time_ns();
m_read_lock_wait_time += (end_ns - start_ns);
m_read_lock_wait_count++;
return std::move(mux_guard);
}

/**
* \brief Lock the mutex, allowing exclusive write access to the stored object
*
* The returned guard object allows access to the protected data
* via operator * or -> and ensures the lock is held as long as
* the guard object exists
*/
SharedMutexGuard<T> write_lock()
{
auto start_ns = sinsp_utils::get_current_time_ns();
auto mux_guard = SharedMutexGuard<T>(std::unique_lock<std::shared_mutex>(m_shared_lock), &m_inner);
auto end_ns = sinsp_utils::get_current_time_ns();
m_write_lock_wait_time += (end_ns - start_ns);
m_write_lock_wait_count++;
return std::move(mux_guard);
}

time_type get_avg_read_lock_wait_time() const { return (m_read_lock_wait_time / m_read_lock_wait_count); }

time_type get_avg_write_lock_wait_time() const { return (m_write_lock_wait_time / m_write_lock_wait_count); }

private:
mutable std::mutex m_lock;
mutable std::shared_mutex m_shared_lock;
T m_inner;
mutable time_type m_read_lock_wait_time = 0;
mutable time_type m_write_lock_wait_time = 0;
mutable time_type m_read_lock_wait_count = 0;
mutable time_type m_write_lock_wait_count = 0;
};
}

} // namespace libsinsp

0 comments on commit 361b970

Please sign in to comment.