From 0e049a9951af7fadefe070ab8e3eb492d978bf18 Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Thu, 20 Feb 2025 13:23:29 +0100 Subject: [PATCH] Feature: rust-like `mutex` and `shared_mutex` (#74) --- README.md | 6 + examples/CMakeLists.txt | 16 ++ examples/example_mutex.cpp | 15 ++ examples/example_shared_mutex.cpp | 22 +++ include/dice/template-library/mutex.hpp | 134 ++++++++++++++ .../dice/template-library/shared_mutex.hpp | 163 ++++++++++++++++++ tests/CMakeLists.txt | 7 + tests/tests_mutex.cpp | 70 ++++++++ tests/tests_shared_mutex.cpp | 97 +++++++++++ 9 files changed, 530 insertions(+) create mode 100644 examples/example_mutex.cpp create mode 100644 examples/example_shared_mutex.cpp create mode 100644 include/dice/template-library/mutex.hpp create mode 100644 include/dice/template-library/shared_mutex.hpp create mode 100644 tests/tests_mutex.cpp create mode 100644 tests/tests_shared_mutex.cpp diff --git a/README.md b/README.md index 0c23cb8..73711d8 100644 --- a/README.md +++ b/README.md @@ -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 @@ -107,6 +108,11 @@ Additionally, `visit` does not involve any virtual function calls. ### `type_traits.hpp` Things that are missing in the standard library `` 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 diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 26653ab..db791f3 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -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 +) + diff --git a/examples/example_mutex.cpp b/examples/example_mutex.cpp new file mode 100644 index 0000000..6b94307 --- /dev/null +++ b/examples/example_mutex.cpp @@ -0,0 +1,15 @@ +#include + +#include +#include + +int main() { + dice::template_library::mutex mut{0}; + + std::thread thrd{[&]() { + *mut.lock() = 5; + }}; + + thrd.join(); + assert(*mut.lock() == 5); +} diff --git a/examples/example_shared_mutex.cpp b/examples/example_shared_mutex.cpp new file mode 100644 index 0000000..a48c132 --- /dev/null +++ b/examples/example_shared_mutex.cpp @@ -0,0 +1,22 @@ +#include + +#include +#include + +int main() { + dice::template_library::shared_mutex mut{0}; + + std::thread thrd1{[&]() { + *mut.lock() = 5; + }}; + + thrd1.join(); + + std::thread thrd2{[&]() { + assert(*mut.lock_shared() == 5); + }}; + + thrd2.join(); + + assert(*mut.lock() == 5); +} diff --git a/include/dice/template-library/mutex.hpp b/include/dice/template-library/mutex.hpp new file mode 100644 index 0000000..39746f0 --- /dev/null +++ b/include/dice/template-library/mutex.hpp @@ -0,0 +1,134 @@ +#ifndef DICE_TEMPLATELIBRARY_MUTEX_HPP +#define DICE_TEMPLATELIBRARY_MUTEX_HPP + +#include +#include +#include +#include + +namespace dice::template_library { + + template + 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 + struct mutex_guard { + using value_type = T; + using mutex_type = Mutex; + + private: + friend struct mutex; + + value_type *value_ptr_; + std::unique_lock lock_; + + mutex_guard(std::unique_lock &&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>` or `std::shared_ptr>` + * + * @tparam T value type stored + * @tparam Mutex the mutex type + */ + template + 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) = default; + constexpr ~mutex() noexcept(std::is_nothrow_destructible_v) = 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_{value} { + } + + explicit constexpr mutex(value_type &&value) noexcept(std::is_nothrow_move_constructible_v) + : value_{std::move(value)} { + } + + template + explicit constexpr mutex(std::in_place_t, Args &&...args) noexcept(std::is_nothrow_constructible_v(args))...>) + : value_{std::forward(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 lock() { + return mutex_guard{std::unique_lock{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> try_lock() { + std::unique_lock lock{mutex_, std::try_to_lock}; + if (!lock.owns_lock()) { + return std::nullopt; + } + + return mutex_guard{std::move(lock), value_}; + } + }; + +} // namespace dice::template_library + +#endif // DICE_TEMPLATELIBRARY_MUTEX_HPP diff --git a/include/dice/template-library/shared_mutex.hpp b/include/dice/template-library/shared_mutex.hpp new file mode 100644 index 0000000..61d4b1f --- /dev/null +++ b/include/dice/template-library/shared_mutex.hpp @@ -0,0 +1,163 @@ +#ifndef DICE_TEMPLATELIBRARY_SHAREDMUTEX_HPP +#define DICE_TEMPLATELIBRARY_SHAREDMUTEX_HPP + +#include +#include +#include +#include +#include + +namespace dice::template_library { + + template + 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 + struct shared_mutex_guard { + using value_type = T; + using mutex_type = Mutex; + using lock_type = std::conditional_t, std::shared_lock, std::unique_lock>; + + private: + friend struct shared_mutex, 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>` or `std::shared_ptr>` + * + * @tparam T value type stored + * @tparam Mutex the mutex type + */ + template + 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) = default; + constexpr ~shared_mutex() noexcept(std::is_nothrow_destructible_v) = 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_{value} { + } + + explicit constexpr shared_mutex(value_type &&value) noexcept(std::is_nothrow_move_constructible_v) + : value_{std::move(value)} { + } + + template + explicit constexpr shared_mutex(std::in_place_t, Args &&...args) noexcept(std::is_nothrow_constructible_v(args))...>) + : value_{std::forward(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 lock() { + return shared_mutex_guard{std::unique_lock{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> try_lock() { + std::unique_lock lock{mutex_, std::try_to_lock}; + if (!lock.owns_lock()) { + return std::nullopt; + } + + return shared_mutex_guard{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 lock_shared() { + return shared_mutex_guard{std::shared_lock{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> try_lock_shared() { + std::shared_lock lock{mutex_, std::try_to_lock}; + if (!lock.owns_lock()) { + return std::nullopt; + } + + return shared_mutex_guard{std::move(lock), value_}; + } + }; + +} // namespace dice::template_library + +#endif // DICE_TEMPLATELIBRARY_SHAREDMUTEX_HPP diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8f39bea..0497ac1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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) + diff --git a/tests/tests_mutex.cpp b/tests/tests_mutex.cpp new file mode 100644 index 0000000..c506600 --- /dev/null +++ b/tests/tests_mutex.cpp @@ -0,0 +1,70 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include + +#include +#include + +TEST_SUITE("mutex") { + using namespace dice::template_library; + + static_assert(!std::is_copy_constructible_v>); + static_assert(!std::is_move_constructible_v>); + static_assert(!std::is_copy_assignable_v>); + static_assert(!std::is_move_assignable_v>); + + TEST_CASE("defalt ctor") { + mutex mut{}; + CHECK_EQ(*mut.lock(), 0); + } + + TEST_CASE("copy value inside") { + std::vector vec{1, 2, 3}; + mutex> mut{vec}; + CHECK_EQ(mut.lock()->size(), 3); + } + + TEST_CASE("move value inside") { + std::vector vec{1, 2, 3}; + mutex> mut{std::move(vec)}; + CHECK_EQ(mut.lock()->size(), 3); + } + + TEST_CASE("in place construction") { + struct data { + int x; + double y; + }; + + mutex mut{std::in_place, 5, 12.2}; + CHECK_EQ(mut.lock()->x, 5); + CHECK_EQ(mut.lock()->y, 12.2); + } + + TEST_CASE("swap") { + mutex a{1}; + mutex 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 mut{}; + CHECK_EQ(*mut.lock(), 0); + } + + TEST_CASE("try_lock compiles") { + mutex mut{}; + auto guard = mut.try_lock(); + CHECK(guard.has_value()); + CHECK_EQ(**guard, 0); + } +} diff --git a/tests/tests_shared_mutex.cpp b/tests/tests_shared_mutex.cpp new file mode 100644 index 0000000..717a65a --- /dev/null +++ b/tests/tests_shared_mutex.cpp @@ -0,0 +1,97 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include + +#include +#include + +TEST_SUITE("shared_mutex") { + using namespace dice::template_library; + + static_assert(!std::is_copy_constructible_v>); + static_assert(!std::is_move_constructible_v>); + static_assert(!std::is_copy_assignable_v>); + static_assert(!std::is_move_assignable_v>); + + TEST_CASE("defalt ctor") { + shared_mutex mut{}; + CHECK_EQ(*mut.lock(), 0); + } + + TEST_CASE("copy value inside") { + std::vector vec{1, 2, 3}; + shared_mutex> mut{vec}; + CHECK_EQ(mut.lock()->size(), 3); + } + + TEST_CASE("move value inside") { + std::vector vec{1, 2, 3}; + shared_mutex> mut{std::move(vec)}; + CHECK_EQ(mut.lock()->size(), 3); + } + + TEST_CASE("in place construction") { + struct data { + int x; + double y; + }; + + shared_mutex 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 a{1}; + shared_mutex 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 mut{}; + CHECK_EQ(*mut.lock(), 0); + } + + TEST_CASE("try_lock compiles") { + shared_mutex mut{}; + auto guard = mut.try_lock(); + CHECK(guard.has_value()); + CHECK_EQ(**guard, 0); + } + + TEST_CASE("lock_shared compiles") { + shared_mutex mut{}; + CHECK_EQ(*mut.lock_shared(), 0); + } + + TEST_CASE("try_lock_shared compiles") { + shared_mutex mut{}; + auto guard = mut.try_lock_shared(); + CHECK(guard.has_value()); + CHECK_EQ(**guard, 0); + } +}