Skip to content

Commit

Permalink
Feature: rust-like mutex and shared_mutex (#74)
Browse files Browse the repository at this point in the history
  • Loading branch information
liss-h authored Feb 20, 2025
1 parent 4e3fd89 commit 0e049a9
Show file tree
Hide file tree
Showing 9 changed files with 530 additions and 0 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +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`/`shared_mutex`: Rust inspired mutex interfaces that hold their data instead of living next to it

## Usage

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

### `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 holding the mutex.

### Further Examples

Compilable code examples can be found in [examples](./examples). The example build requires the cmake
Expand Down
16 changes: 16 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,19 @@ target_link_libraries(example_pool_allocator
PRIVATE
dice-template-library::dice-template-library
)


add_executable(example_mutex
example_mutex.cpp)
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
)

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

#include <cassert>
#include <thread>

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

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

thrd.join();
assert(*mut.lock() == 5);
}
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);
}
134 changes: 134 additions & 0 deletions include/dice/template-library/mutex.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#ifndef DICE_TEMPLATELIBRARY_MUTEX_HPP
#define DICE_TEMPLATELIBRARY_MUTEX_HPP

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

namespace dice::template_library {

template<typename T, typename Mutex = std::mutex>
struct mutex;

/**
* An RAII guard for a value behind a mutex.
* When this 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::mutex>
struct mutex_guard {
using value_type = T;
using mutex_type = Mutex;

private:
friend struct mutex<T, Mutex>;

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

mutex_guard(std::unique_lock<mutex_type> &&lock, T &value) noexcept
: value_ptr_{&value},
lock_{std::move(lock)} {
}

public:
mutex_guard() = delete;
mutex_guard(mutex_guard const &other) noexcept = delete;
mutex_guard &operator=(mutex_guard const &other) noexcept = delete;
mutex_guard(mutex_guard &&other) noexcept = default;
mutex_guard &operator=(mutex_guard &&other) noexcept = default;
~mutex_guard() = default;

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

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_);
}
};

/**
* A rust-like mutex type (https://doc.rust-lang.org/std/sync/struct.Mutex.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<mutex<T>>` or `std::shared_ptr<mutex<T>>`
*
* @tparam T value type stored
* @tparam Mutex the mutex type
*/
template<typename T, typename Mutex>
struct mutex {
using value_type = T;
using mutex_type = Mutex;

private:
value_type value_;
mutex_type mutex_;

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

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

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

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

template<typename ...Args>
explicit constexpr 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]] mutex_guard<value_type> lock() {
return 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<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 mutex_guard<value_type>{std::move(lock), value_};
}
};

} // namespace dice::template_library

#endif // DICE_TEMPLATELIBRARY_MUTEX_HPP
163 changes: 163 additions & 0 deletions include/dice/template-library/shared_mutex.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#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 {

template<typename T, typename Mutex = std::shared_mutex>
struct shared_mutex;

/**
* 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:
friend struct shared_mutex<std::remove_const_t<T>, Mutex>;

value_type *value_ptr_;
lock_type lock_;

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

public:
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>
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
7 changes: 7 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,10 @@ custom_add_test(tests_limit_allocator)

add_executable(tests_pool_allocator tests_pool_allocator.cpp)
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)

Loading

0 comments on commit 0e049a9

Please sign in to comment.