Skip to content

Commit

Permalink
add shared mutex
Browse files Browse the repository at this point in the history
  • Loading branch information
liss-h committed Feb 20, 2025
1 parent 1805051 commit 31e0f3e
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 6 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ It contains:
- `generator`: The reference implementation of `std::generator` from [P2502R2](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2502r2.pdf)
- `channel`: A single producer, single consumer queue
- `variant2`: Like `std::variant` but optimized for exactly two types
- `mutex`: A rust inspired mutex interface that holds its data instead of living next to it
- `mutex`/`shared_mutex`: Rust inspired mutex interfaces that hold their data instead of living next to it

## Usage

Expand Down Expand Up @@ -108,10 +108,10 @@ Additionally, `visit` does not involve any virtual function calls.
### `type_traits.hpp`
Things that are missing in the standard library `<type_traits>` header.

### `mutex`
A rust inspired mutex interface that holds its data instead of living next to it.
### `mutex`/`shared_mutex`
Rust inspired mutex interfaces that hold their data instead of living next to it.
The benefit of this approach is that it makes it harder (impossible in rust) to access the
data without having exclusive access to it.
data without holding the mutex.

### Further Examples

Expand Down
8 changes: 8 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,11 @@ target_link_libraries(example_mutex
PRIVATE
dice-template-library::dice-template-library
)

add_executable(example_shared_mutex
example_shared_mutex.cpp)
target_link_libraries(example_shared_mutex
PRIVATE
dice-template-library::dice-template-library
)

22 changes: 22 additions & 0 deletions examples/example_shared_mutex.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#include <dice/template-library/shared_mutex.hpp>

#include <cassert>
#include <thread>

int main() {
dice::template_library::shared_mutex<int> mut{0};

std::thread thrd1{[&]() {
*mut.lock() = 5;
}};

thrd1.join();

std::thread thrd2{[&]() {
assert(*mut.lock_shared() == 5);
}};

thrd2.join();

assert(*mut.lock() == 5);
}
8 changes: 7 additions & 1 deletion include/dice/template-library/mutex.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ namespace dice::template_library {
using mutex_type = Mutex;

private:
T *value_ptr_;
value_type *value_ptr_;
std::unique_lock<mutex_type> lock_;

public:
Expand All @@ -48,6 +48,12 @@ namespace dice::template_library {
value_type &operator*() const noexcept {
return *value_ptr_;
}

friend void swap(mutex_guard const &lhs, mutex_guard const &rhs) noexcept {
using std::swap;
swap(lhs.value_ptr_, rhs.value_ptr_);
swap(lhs.lock_, rhs.lock_);
}
};

/**
Expand Down
158 changes: 158 additions & 0 deletions include/dice/template-library/shared_mutex.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#ifndef DICE_TEMPLATELIBRARY_SHAREDMUTEX_HPP
#define DICE_TEMPLATELIBRARY_SHAREDMUTEX_HPP

#include <mutex>
#include <shared_mutex>
#include <optional>
#include <type_traits>
#include <utility>

namespace dice::template_library {

/**
* An RAII guard for a value behind a shared_mutex.
* When this shared_mutex_guard is dropped the lock is automatically released.
*
* @note to use this correctly it is important to understand that unlike rust, C++ cannot
* enforce that you do not use pointers given out by this type beyond its lifetime.
* You may not store pointers or references given out via this wrapper beyond its lifetime, otherwise behaviour is undefined.
*
* @tparam T the value type protected by the mutex
* @tparam Mutex the mutex type
*/
template<typename T, typename Mutex = std::shared_mutex>
struct shared_mutex_guard {
using value_type = T;
using mutex_type = Mutex;
using lock_type = std::conditional_t<std::is_const_v<T>, std::shared_lock<mutex_type>, std::unique_lock<mutex_type>>;

private:
value_type *value_ptr_;
lock_type lock_;

public:
shared_mutex_guard(lock_type &&lock, T &value) noexcept
: value_ptr_{&value},
lock_{std::move(lock)} {
}

shared_mutex_guard() = delete;
shared_mutex_guard(shared_mutex_guard const &other) noexcept = delete;
shared_mutex_guard &operator=(shared_mutex_guard const &other) noexcept = delete;
shared_mutex_guard(shared_mutex_guard &&other) noexcept = default;
shared_mutex_guard &operator=(shared_mutex_guard &&other) noexcept = default;
~shared_mutex_guard() = default;

value_type *operator->() const noexcept {
return value_ptr_;
}

value_type &operator*() const noexcept {
return *value_ptr_;
}

friend void swap(shared_mutex_guard const &lhs, shared_mutex_guard const &rhs) noexcept {
using std::swap;
swap(lhs.value_ptr_, rhs.value_ptr_);
swap(lhs.lock_, rhs.lock_);
}
};

/**
* A rust-like shared_mutex type (https://doc.rust-lang.org/std/sync/struct.RwLock.html) that holds its data instead of living next to it.
*
* @note Because this is C++ it cannot be fully safe, like the rust version is, for more details see doc comment on mutex_guard.
* @note This type is non-movable and non-copyable, if you need to do either of these things use `std::unique_ptr<shared_mutex<T>>` or `std::shared_ptr<shared_mutex<T>>`
*
* @tparam T value type stored
* @tparam Mutex the mutex type
*/
template<typename T, typename Mutex = std::shared_mutex>
struct shared_mutex {
using value_type = T;
using mutex_type = Mutex;

private:
value_type value_;
mutex_type mutex_;

public:
constexpr shared_mutex() noexcept(std::is_nothrow_default_constructible_v<value_type>) = default;
constexpr ~shared_mutex() noexcept(std::is_nothrow_destructible_v<value_type>) = default;

shared_mutex(shared_mutex const &other) = delete;
shared_mutex(shared_mutex &&other) = delete;
shared_mutex &operator=(shared_mutex const &other) = delete;
shared_mutex &operator=(shared_mutex &&other) = delete;

explicit constexpr shared_mutex(value_type const &value) noexcept(std::is_nothrow_copy_constructible_v<value_type>)
: value_{value} {
}

explicit constexpr shared_mutex(value_type &&value) noexcept(std::is_nothrow_move_constructible_v<value_type>)
: value_{std::move(value)} {
}

template<typename ...Args>
explicit constexpr shared_mutex(std::in_place_t, Args &&...args) noexcept(std::is_nothrow_constructible_v<value_type, decltype(std::forward<Args>(args))...>)
: value_{std::forward<Args>(args)...} {
}

/**
* Lock the mutex and return a guard that will keep it locked until it goes out of scope and
* allows access to the inner value.
*
* @return mutex guard for the inner value
* @throws std::system_error in case the underlying mutex implementation throws it
*/
[[nodiscard]] shared_mutex_guard<value_type> lock() {
return shared_mutex_guard<value_type>{std::unique_lock<mutex_type>{mutex_}, value_};
}

/**
* Attempt to lock the mutex and return a guard that will keep it locked until it goes out of scope and
* allows access to the inner value.
*
* @return nullopt in case the mutex could not be locked, otherwise a mutex guard for the inner value
* @throws std::system_error in case the underlying mutex implementation throws it
*/
[[nodiscard]] std::optional<shared_mutex_guard<value_type>> try_lock() {
std::unique_lock<mutex_type> lock{mutex_, std::try_to_lock};
if (!lock.owns_lock()) {
return std::nullopt;
}

return shared_mutex_guard<value_type>{std::move(lock), value_};
}

/**
* Lock the mutex for shared ownership and return a guard that will keep it locked until it goes out of scope and
* allows access to the inner value.
*
* @return mutex guard for the inner value
* @throws std::system_error in case the underlying mutex implementation throws it
*/
[[nodiscard]] shared_mutex_guard<value_type const> lock_shared() {
return shared_mutex_guard<value_type const>{std::shared_lock<mutex_type>{mutex_}, value_};
}

/**
* Attempt to lock the mutex for shared ownership and return a guard that will keep it locked until it goes out of scope and
* allows access to the inner value.
*
* @return nullopt in case the mutex could not be locked, otherwise a mutex guard for the inner value
* @throws std::system_error in case the underlying mutex implementation throws it
*/
[[nodiscard]] std::optional<shared_mutex_guard<value_type const>> try_lock_shared() {
std::shared_lock<mutex_type> lock{mutex_, std::try_to_lock};
if (!lock.owns_lock()) {
return std::nullopt;
}

return shared_mutex_guard<value_type const>{std::move(lock), value_};
}
};

} // namespace dice::template_library

#endif // DICE_TEMPLATELIBRARY_SHAREDMUTEX_HPP
4 changes: 4 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,7 @@ custom_add_test(tests_pool_allocator)

add_executable(tests_mutex tests_mutex.cpp)
custom_add_test(tests_mutex)

add_executable(tests_shared_mutex tests_shared_mutex.cpp)
custom_add_test(tests_shared_mutex)

18 changes: 17 additions & 1 deletion tests/tests_mutex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,28 @@ TEST_SUITE("mutex") {
CHECK_EQ(mut.lock()->y, 12.2);
}

TEST_CASE("swap") {
mutex<int> a{1};
mutex<int> b{2};

auto ga = a.lock();
auto gb = b.lock();

CHECK_EQ(*ga, 1);
CHECK_EQ(*gb, 2);

swap(ga, gb);

CHECK_EQ(*ga, 2);
CHECK_EQ(*gb, 1);
}

TEST_CASE("lock compiles") {
mutex<int> mut{};
CHECK_EQ(*mut.lock(), 0);
}

TEST_CASE("try lock compiles") {
TEST_CASE("try_lock compiles") {
mutex<int> mut{};
auto guard = mut.try_lock();
CHECK(guard.has_value());
Expand Down
104 changes: 104 additions & 0 deletions tests/tests_shared_mutex.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>

#include <dice/template-library/shared_mutex.hpp>
#include <type_traits>

TEST_SUITE("shared_mutex") {
using namespace dice::template_library;

static_assert(!std::is_copy_constructible_v<shared_mutex<int>>);
static_assert(!std::is_move_constructible_v<shared_mutex<int>>);
static_assert(!std::is_copy_assignable_v<shared_mutex<int>>);
static_assert(!std::is_move_assignable_v<shared_mutex<int>>);

TEST_CASE("defalt ctor") {
{ // normal init
shared_mutex<int> mut{};
CHECK_EQ(*mut.lock(), 0);
}

{ // constexpr init
static constinit shared_mutex<int> cmut{};
CHECK_EQ(*cmut.lock(), 0);
}
}

TEST_CASE("copy value inside") {
std::vector<int> vec{1, 2, 3};
shared_mutex<std::vector<int>> mut{vec};
CHECK_EQ(mut.lock()->size(), 3);
}

TEST_CASE("move value inside") {
std::vector<int> vec{1, 2, 3};
shared_mutex<std::vector<int>> mut{std::move(vec)};
CHECK_EQ(mut.lock()->size(), 3);
}

TEST_CASE("in place construction") {
struct data {
int x;
double y;
};

shared_mutex<data> mut{std::in_place, 5, 12.2};
CHECK_EQ(mut.lock()->x, 5);
CHECK_EQ(mut.lock()->y, 12.2);
}

TEST_CASE("swap") {
shared_mutex<int> a{1};
shared_mutex<int> b{2};

SUBCASE("lock") {
auto ga = a.lock();
auto gb = b.lock();

CHECK_EQ(*ga, 1);
CHECK_EQ(*gb, 2);

swap(ga, gb);

CHECK_EQ(*ga, 2);
CHECK_EQ(*gb, 1);
}

SUBCASE("lock_shared") {
auto ga = a.lock_shared();
auto gb = b.lock_shared();

CHECK_EQ(*ga, 1);
CHECK_EQ(*gb, 2);

swap(ga, gb);

CHECK_EQ(*ga, 2);
CHECK_EQ(*gb, 1);
}
}

TEST_CASE("lock compiles") {
shared_mutex<int> mut{};
CHECK_EQ(*mut.lock(), 0);
}

TEST_CASE("try_lock compiles") {
shared_mutex<int> mut{};
auto guard = mut.try_lock();
CHECK(guard.has_value());
CHECK_EQ(**guard, 0);
}

TEST_CASE("lock_shared compiles") {
shared_mutex<int> mut{};
CHECK_EQ(*mut.lock_shared(), 0);
}

TEST_CASE("try_lock_shared compiles") {
shared_mutex<int> mut{};
auto guard = mut.try_lock_shared();
CHECK(guard.has_value());
CHECK_EQ(**guard, 0);
}
}

0 comments on commit 31e0f3e

Please sign in to comment.