`synchronized_value` (#3984)

This commit is contained in:
Klaim (Joël Lamotte) 2025-06-24 11:51:45 +02:00 committed by GitHub
parent ffd08b8661
commit 98b48779e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1099 additions and 26 deletions

View File

@ -316,6 +316,7 @@ set(
${LIBMAMBA_INCLUDE_DIR}/mamba/util/path_manip.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/util/random.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/util/string.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/util/synchronized_value.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/util/tuple_hash.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/util/type_traits.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/util/url_manip.hpp

View File

@ -0,0 +1,629 @@
// Copyright (c) 2025, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_UTIL_SYNCHRONIZED_VALUE_HPP
#define MAMBA_UTIL_SYNCHRONIZED_VALUE_HPP
#include <concepts>
#include <mutex>
#include <shared_mutex>
#include <tuple>
namespace mamba::util
{
/////////////////////////////
// TODO: move that in a more general location
// original: https://github.com/man-group/sparrow/blob/main/include/sparrow/utils/mp_utils.hpp
template <class L, template <class...> class U>
struct is_type_instance_of : std::false_type
{
};
template <template <class...> class L, template <class...> class U, class... T>
requires std::same_as<L<T...>, U<T...>>
struct is_type_instance_of<L<T...>, U> : std::true_type
{
};
/// `true` if `T` is a concrete type template instantiation of `U` which is a type template.
/// Example: is_type_instance_of_v< std::vector<int>, std::vector > == true
template <class T, template <class...> class U>
constexpr bool is_type_instance_of_v = is_type_instance_of<T, U>::value;
/////////////////////////////
/// see https://en.cppreference.com/w/cpp/named_req/BasicLockable.html
template <class T>
concept BasicLockable = requires(T& x) {
x.lock();
x.unlock();
};
// and noexcept(T{}.unlock());
/// see https://en.cppreference.com/w/cpp/named_req/LockableMutex.html
template <class T>
concept Lockable = BasicLockable<T> and requires(T& x) {
{ x.try_lock() } -> std::convertible_to<bool>;
};
/// see https://en.cppreference.com/w/cpp/named_req/Mutex.html
template <class T>
concept Mutex = Lockable<T> and std::default_initializable<T> and std::destructible<T>
and (not std::movable<T>) and (not std::copyable<T>);
/// see https://en.cppreference.com/w/cpp/named_req/SharedMutex.html
template <class T>
concept SharedMutex = Mutex<T> and requires(T& x) {
x.lock_shared();
{ x.try_lock_shared() } -> std::convertible_to<bool>;
x.unlock_shared();
};
/** Locks a mutex object using the most constrained sharing lock available for that mutex type.
@returns A scoped locking object. The exact type depends on the mutex type.
*/
template <Mutex M>
[[nodiscard]]
auto lock_as_readonly(M& mutex)
{
return std::unique_lock{ mutex };
}
template <SharedMutex M>
[[nodiscard]]
auto lock_as_readonly(M& mutex)
{
return std::shared_lock{ mutex };
}
/** Locks multiple mutex objects using the most constrained sharing lock available for that
mutex type.
@returns A tuple of scoped locking objects, one for each mutex. The exact types depends on
the mutex types.
*/
template <Mutex... M>
requires(sizeof...(M) > 1)
[[nodiscard]]
auto lock_as_readonly(M&... mutex)
{
return std::make_tuple(lock_as_readonly(mutex)...);
}
/** Locks multiple non-shared mutex objects using the most constrained sharing lock available
for that mutex type.
@returns A scoped locking object.
*/
template <Mutex... M>
requires(sizeof...(M) > 1) and ((not SharedMutex<M>) and ...)
[[nodiscard]] auto lock_as_readonly(M&... mutex)
{
return std::scoped_lock{ mutex... };
}
/** Locks a mutex object using an exclusive lock.
@returns A scoped locking object.
*/
template <Mutex M>
[[nodiscard]]
auto lock_as_exclusive(M& mutex)
{
return std::unique_lock{ mutex };
}
/** Locks multiple mutex objects using an exclusive lock.
@returns A scoped locking object.
*/
template <Mutex... M>
requires(sizeof...(M) > 1)
[[nodiscard]]
auto lock_as_exclusive(M&... mutex)
{
return std::scoped_lock{ mutex... };
}
namespace details
{
template <typename T>
T& ref_of(); // used only in non-executed contexts
}
/** Scoped locking type that would result from locking the provided mutex in the most
constrained way.
*/
template <Mutex M, bool readonly>
using lock_type = std::conditional_t<
readonly,
decltype(lock_as_readonly(details::ref_of<M>())),
decltype(lock_as_exclusive(details::ref_of<M>()))>;
/** Locks a mutex for the lifetime of this type's instance and provide access to an associated
value.
If `readonly == true`, only non-mutable access to the associated value will be provided.
The access to the value is pointer-like, but this type does not own or copy that value,
it is accessed directly.
*/
template <std::default_initializable T, Mutex M, bool readonly>
class [[nodiscard]] scoped_locked_ptr
{
std::conditional_t<readonly, const T*, T*> m_value;
lock_type<M, readonly> m_lock;
public:
static constexpr bool is_readonly = readonly;
/** Locks the provided mutex immediately.
The provided value will then be accessible as mutable through the member functions.
*/
scoped_locked_ptr(T& value, M& mutex)
requires(not readonly)
: m_value(&value)
, m_lock(mutex)
{
}
/** Locks the provided mutex immediately.
The provided value will then be accessible as non-mutable through the member functions.
*/
scoped_locked_ptr(const T& value, M& mutex)
requires(readonly)
: m_value(&value)
, m_lock(mutex)
{
}
scoped_locked_ptr(scoped_locked_ptr&& other) noexcept
// Both objects are locking at this point, so it is safe to modify both values.
: m_value(std::move(other.m_value))
, m_lock(std::move(other.m_lock))
{
other.m_value = nullptr;
}
scoped_locked_ptr& operator=(scoped_locked_ptr&& other) noexcept
{
// Both objects are locking at this point, so it is safe to modify both values.
m_value = std::move(other.m_value);
other.m_value = nullptr;
return *this;
}
[[nodiscard]] auto operator*() -> T& requires(not readonly) { return *m_value; }
[[nodiscard]] auto operator*() const -> const T&
{
return *m_value;
}
[[nodiscard]] auto
operator->() -> T* requires(not readonly) { return m_value; }
[[nodiscard]] auto operator->() const -> const T*
{
return m_value;
}
};
/** Thread-safe value storage.
Holds an object which access is always implying a lock to an associated mutex.
The only access to the object without a lock are "unsafe" functions, which are marked as
such. Also provides ways to lock the access to the object in scopes. Mainly used when a
value needs to be protected by a mutex and we want to make sure the code always does the
right locking mechanism.
If the mutex type satisfies `SharedMutex`, the locks will be shared if using `const`
functions, enabling cheaper read-only access to the object in that context.
Some operations will lock for the time of the call, others (like `operator->`) will
return a `scoped_locked_ptr` so that the lock will hold for a whole expression or
a bigger scope. `synchronize()` explicitly only builds such scoped-lock and provides it
for scoped usage of the object.
Note: this is inspired by boost::thread::synchronized_value and the C++ Concurrent TS 2
paper, refer to these to compare the features and correctness.
*/
template <std::default_initializable T, Mutex M = std::mutex>
class synchronized_value
{
public:
using value_type = T;
using mutex_type = M;
using this_type = synchronized_value<T, M>;
synchronized_value() noexcept(std::is_nothrow_default_constructible_v<T>);
// NOTE FOR FUTURE DEVS: move operations could be implemented correctly by interpreting it
// as moving the stored object after locking `other` first. However,
// right now we have no need for this and it might be misleading semantics (we are
// not moving the `synchronized_value` per-say, we have moving only it's value).
// If the need comes, implement it correctly as a lock followed by a `T` move.
// Do not move the `other.m_mutex`, do not `= default;` as these solutions are
// incorrect.
/** Moves are forbidden so that the mutex can protect the memory region represented
by the stored object.
*/
synchronized_value(synchronized_value&& other) noexcept = delete;
synchronized_value& operator=(synchronized_value&& other) noexcept = delete;
/// Constructs with a provided value as initializer for the stored object.
template <typename V>
requires std::assignable_from<T&, V> and (not std::same_as<this_type, std::decay_t<V>>)
synchronized_value(V&& value) noexcept
: m_value(std::forward<V>(value))
{
// NOTE: when moving the definition outside the class,
// VS2022 will not match the declaration with the definition
// which is probably a bug. To workaround that we keep
// the definition here.
}
/// Constructs with a provided initializer list used to initialize the stored object.
template <typename V>
requires std::constructible_from<T, std::initializer_list<V>>
synchronized_value(std::initializer_list<V> values);
/** Locks the provided `synchronized_value`'s mutex and copies it's stored object value
to this instance's stored object.
The lock is released before the end of the call.
If `SharedMutex<M> == true`, the lock is a shared-lock for the provided
`synchronized_value`'s mutex.
*/
synchronized_value(const synchronized_value& other);
/** Locks both mutexes and copies/move the value of the provided `synchronized_value`'s
stored object to this instance's stored object. The lock is released before the end of
the call. If `SharedMutex<M> == true`, the lock is a shared-lock for the provided
`synchronized_value`'s mutex.
*/
template <std::equality_comparable_with<T> U, Mutex OtherMutex>
auto operator=(const synchronized_value<U, OtherMutex>& other) -> synchronized_value&;
/** Locks and assign the provided value to the stored object.
The lock is released before the end of the call.
*/
template <typename V>
requires std::assignable_from<T&, V> and (not std::same_as<this_type, std::decay_t<V>>)
auto operator=(V&& value) noexcept -> synchronized_value&
{
// NOTE: when moving the definition outside the class,
// VS2022 will not match the declaration with the definition
// which is probably a bug. To workaround that we keep
// the definition here.
auto _ = lock_as_exclusive(m_mutex);
m_value = std::forward<V>(value);
return *this;
}
/** Locks and return the value of the current object.
The lock is released before the end of the call.
If `SharedMutex<M> == true`, the lock is a shared-lock.
*/
[[nodiscard]]
auto value() const -> T;
/** Locks and return the value of the current object.
The lock is released before the end of the call.
If `SharedMutex<M> == true`, the lock is a shared-lock.
*/
[[nodiscard]]
explicit operator T() const
{
return value();
}
/** Not-thread-safe access to the stored object.
Only used this for testing purposes.
*/
[[nodiscard]]
auto unsafe_get() const -> const T&
{
return m_value;
}
/** Not-thread-safe access to the stored object.
Only used this for testing purposes.
*/
[[nodiscard]]
auto unsafe_get() -> T&
{
return m_value;
}
using locked_ptr = scoped_locked_ptr<T, M, false>;
using const_locked_ptr = scoped_locked_ptr<T, M, true>;
/** Locks the mutex and returns a `scoped_locked_ptr` which will provide
mutable access to the stored object, while holding the lock for it's whole
lifetime, which usually for this call is for the time of the expression.
The lock is released only once the returned object is destroyed.
Example:
synchronized_value<std::vector<int>> values;
values->resize(10); // locks, calls `std::vector::resize`, then unlocks.
*/
[[nodiscard]]
auto operator->() -> locked_ptr;
/** Locks the mutex and returns a `scoped_locked_ptr` which will provide
non-mutable access to the stored object, while holding the lock for it's whole
lifetime, which usually for this call is for the time of the expression.
The lock is released only once the returned object is destroyed.
If `SharedMutex<M> == true`, the lock is a shared-lock.
Example:
synchronized_value<std::vector<int>> values;
auto x = values->size(); // locks, calls `std::vector::size`, then unlocks.
*/
[[nodiscard]]
auto operator->() const -> const_locked_ptr;
/** Locks the mutex and returns a `scoped_locked_ptr` which will provide
mutable access to the stored object, while holding the lock for it's whole
lifetime.
The lock is released only once the returned object is destroyed.
This is mainly used to get exclusive mutable access to the stored object for a whole
scope. Example: synchronized_value<std::vector<int>> values;
{
auto sync_values = values.synchronize(); // locks
const auto x = sync_values->size();
sync_values->resize(x);
// ... maybe more mutable operations ...
} // unlocks
*/
[[nodiscard]]
auto synchronize() -> locked_ptr;
/** Locks the mutex and returns a `scoped_locked_ptr` which will provide
non-mutable access to the stored object, while holding the lock for it's whole
lifetime.
The lock is released only once the returned object is destroyed.
If `SharedMutex<M> == true`, the lock is a shared-lock.
This is mainly used to make sure the stored object doesnt change for a whole scope.
Example:
synchronized_value<std::vector<int>> values;
{
auto sync_values = values.synchronize(); // locks
const auto x = sync_values->size();
// ... more non-mutable operations ...
} // unlocks
*/
[[nodiscard]]
auto synchronize() const -> const_locked_ptr;
/** Locks the mutex and calls the provided invocable, passing the mutable stored object
and the other provided values as arguments.
The lock is released after the provided invocable returns but before this function
returns.
This is mainly used to safely execute an already existing function taking the stored
object as parameter. Example:
synchronized_value<std::vector<int>> values{ random_values };
values.apply(std::ranges::sort); // locks, sort, unlocks
values.apply(std::ranges::sort, std::ranges::greater{}); // locks, reverse sort,
// unlocks
values.apply([](std::vector<int>& vs, auto& input){ // locks
for(int& value : vs)
input >> value;
}], file_stream); // unlocks
*/
template <typename Func, typename... Args>
requires std::invocable<Func, T&, Args...>
auto apply(Func&& func, Args&&... args);
/** Locks the mutex and calls the provided invocable, passing the non-mutable stored object
and the other provided values as arguments.
The lock is released after the provided invocable returns but before this function
returns. If `SharedMutex<M> == true`, the lock is a shared-lock.
This is mainly used to safely execute an already existing function taking the stored
object as parameter. Example:
synchronized_value<std::vector<int>> values{ random_values };
values.apply([](const std::vector<int>& vs, auto& out){ // locks
for(int value : vs)
out << value;
}], file_stream); // unlocks
*/
template <typename Func, typename... Args>
requires std::invocable<Func, T&, Args...>
auto apply(Func&& func, Args&&... args) const;
/// @see `apply()`
template <typename Func, typename... Args>
requires std::invocable<Func, T&, Args...>
auto operator()(Func&& func, Args&&... args)
{
return apply(std::forward<Func>(func), std::forward<Args>(args)...);
}
/// @see `apply()`
template <typename Func, typename... Args>
requires std::invocable<Func, T&, Args...>
auto operator()(Func&& func, Args&&... args) const
{
return apply(std::forward<Func>(func), std::forward<Args>(args)...);
}
// TODO : ADD MORE COMPARISON OPERATORS
/** Locks (shared if possible) and compare equality of the stored object's value with the
provided value.
*/
auto operator==(const std::equality_comparable_with<T> auto& other_value) const -> bool;
/** Locks both (shared if possible) and compare equality of the stored object's value with
the provided value.
*/
template <std::equality_comparable_with<T> U, Mutex OtherMutex>
auto operator==(const synchronized_value<U, OtherMutex>& other_value) const -> bool;
auto swap(synchronized_value& other) -> void;
auto swap(T& value) -> void;
private:
T m_value;
mutable M m_mutex;
};
/** Locks all the provided `synchronized_value` objects using `.synchronize` and
returns the resulting set of `scoped_locked_ptr`.
Used to keep a lock on multiple values at a time under for the lifetime of one same scope.
@see `synchronized_value::synchronize()`
@tparam SynchronizedValues Must be `synchronized_value` type instances.
@param sync_values Various `synchronized_value` objects with potentially different mutex
types and value types. Any of these objects that is provided through
a `const &` will result in a shared-lock for that object.
@returns A tuple of `scoped_locked_ptr`, one for each `sync_values` object, in the same
order. If an object in `sync_values` was passed using `const &`, then for the
associated `scoped_locked_ptr` `scoped_locked_ptr::is_readonly == true`.
*/
template <typename... SynchronizedValues>
requires(is_type_instance_of_v<std::remove_cvref_t<SynchronizedValues>, synchronized_value> and ...)
auto synchronize(SynchronizedValues&&... sync_values);
///////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////
template <std::default_initializable T, Mutex M>
synchronized_value<T, M>::synchronized_value() noexcept(std::is_nothrow_default_constructible_v<T>) = default;
template <std::default_initializable T, Mutex M>
synchronized_value<T, M>::synchronized_value(const synchronized_value& other)
{
auto _ = lock_as_readonly(other.m_mutex);
m_value = other.m_value;
}
template <std::default_initializable T, Mutex M>
template <std::equality_comparable_with<T> U, Mutex OtherMutex>
auto synchronized_value<T, M>::operator=(const synchronized_value<U, OtherMutex>& other)
-> synchronized_value<T, M>&
{
auto this_lock [[maybe_unused]] = lock_as_exclusive(m_mutex);
auto other_lock [[maybe_unused]] = lock_as_readonly(other.m_mutex);
m_value = other.m_value;
return *this;
}
template <std::default_initializable T, Mutex M>
template <typename V>
requires std::constructible_from<T, std::initializer_list<V>>
synchronized_value<T, M>::synchronized_value(std::initializer_list<V> values)
: m_value(std::move(values))
{
}
template <std::default_initializable T, Mutex M>
auto synchronized_value<T, M>::value() const -> T
{
auto _ = lock_as_readonly(m_mutex);
return m_value;
}
template <std::default_initializable T, Mutex M>
auto synchronized_value<T, M>::operator->() -> locked_ptr
{
return locked_ptr{ m_value, m_mutex };
}
template <std::default_initializable T, Mutex M>
auto synchronized_value<T, M>::operator->() const -> const_locked_ptr
{
return const_locked_ptr{ m_value, m_mutex };
}
template <std::default_initializable T, Mutex M>
auto synchronized_value<T, M>::synchronize() -> locked_ptr
{
return locked_ptr{ m_value, m_mutex };
}
template <std::default_initializable T, Mutex M>
auto synchronized_value<T, M>::synchronize() const -> const_locked_ptr
{
return const_locked_ptr{ m_value, m_mutex };
}
template <std::default_initializable T, Mutex M>
template <typename Func, typename... Args>
requires std::invocable<Func, T&, Args...>
auto synchronized_value<T, M>::apply(Func&& func, Args&&... args)
{
auto _ = lock_as_exclusive(m_mutex);
return std::invoke(std::forward<Func>(func), m_value, std::forward<Args>(args)...);
}
template <std::default_initializable T, Mutex M>
template <typename Func, typename... Args>
requires std::invocable<Func, T&, Args...>
auto synchronized_value<T, M>::apply(Func&& func, Args&&... args) const
{
auto _ = lock_as_readonly(m_mutex);
return std::invoke(std::forward<Func>(func), std::as_const(m_value), std::forward<Args>(args)...);
}
template <std::default_initializable T, Mutex M>
auto synchronized_value<T, M>::operator==(const std::equality_comparable_with<T> auto& other_value
) const -> bool
{
auto _ = lock_as_readonly(m_mutex);
return m_value == other_value;
}
template <std::default_initializable T, Mutex M>
template <std::equality_comparable_with<T> U, Mutex OtherMutex>
auto
synchronized_value<T, M>::operator==(const synchronized_value<U, OtherMutex>& other_value) const
-> bool
{
auto _ = lock_as_readonly(m_mutex, other_value.m_mutex);
return m_value == other_value.m_value;
}
template <std::default_initializable T, Mutex M>
auto synchronized_value<T, M>::swap(synchronized_value& other) -> void
{
auto _ = lock_as_exclusive(m_mutex, other.m_mutex);
using std::swap;
swap(m_value, other.m_value);
}
template <std::default_initializable T, Mutex M>
auto synchronized_value<T, M>::swap(T& value) -> void
{
auto _ = lock_as_exclusive(m_mutex);
using std::swap;
swap(m_value, value);
}
template <typename... SynchronizedValues>
requires(is_type_instance_of_v<std::remove_cvref_t<SynchronizedValues>, synchronized_value> and ...)
auto synchronize(SynchronizedValues&&... sync_values)
{
return std::make_tuple(std::forward<SynchronizedValues>(sync_values).synchronize()...);
}
}
#endif

View File

@ -18,6 +18,7 @@ mamba_target_add_compile_warnings(testing_libmamba_lock WARNING_AS_ERROR ${MAMBA
set(
LIBMAMBA_TEST_SRCS
include/mambatests.hpp
include/mambatests_utils.hpp
src/test_main.cpp
# Catch utils
src/catch-utils/conda_url.hpp
@ -41,6 +42,7 @@ set(
src/util/test_parsers.cpp
src/util/test_path_manip.cpp
src/util/test_random.cpp
src/util/test_synchronized_value.cpp
src/util/test_string.cpp
src/util/test_tuple_hash.cpp
src/util/test_type_traits.cpp

View File

@ -0,0 +1,43 @@
// Copyright (c) 2025, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef LIBMAMBATESTS_UTIL_HPP
#define LIBMAMBATESTS_UTIL_HPP
#include <concepts>
#include <functional>
#include <thread>
#include <utility>
namespace mambatests
{
/** Throws a string immediately, used in tests for code that should not be reachable. */
inline void fail_now()
{
throw "this code should never be executed";
}
/** Blocks the current thread until the provided predicates returns `true`.
This is useful to make multiple threads wait on the change of value of a
thread-safe object (for example `std::atomic<bool>`), without having to
do the complicated dance with `std::condition_variable`.
Not recommended to use outside testing purpose.
Calling this will release the thread to the system as much as possible to limit
thread exhaustion. In consequence it is not possible to decide when exactly the
predicate will be evaluated, it depends on when the system resumes the thread.
*/
template <std::predicate<> Predicate>
void wait_condition(Predicate&& predicate)
{
while (!std::invoke(std::forward<Predicate>(predicate)))
{
std::this_thread::yield();
}
}
}
#endif

View File

@ -13,31 +13,13 @@
#include "mamba/core/tasksync.hpp"
#include "mambatests_utils.hpp"
namespace mamba
{
// WARNING: this file will be moved to xtl as soon as possible, do not rely on it's existence
// here!
namespace
{
void fail_now()
{
throw "this code should never be executed";
}
}
namespace
{
template <typename Predicate>
void wait_condition(Predicate&& predicate)
{
while (!std::invoke(std::forward<Predicate>(predicate)))
{
std::this_thread::yield();
}
}
}
namespace
{
TEST_CASE("sync_types_never_move")
@ -78,14 +60,14 @@ namespace mamba
task_sync.join_tasks(); // nothing happen if we call it twice
REQUIRE(task_sync.is_joined());
auto no_op = task_sync.synchronized([] { fail_now(); });
auto no_op = task_sync.synchronized([] { mambatests::fail_now(); });
no_op();
}
TEST_CASE("unexecuted_synched_task_never_blocks_join")
{
TaskSynchronizer task_sync;
auto synched_task = task_sync.synchronized([] { fail_now(); });
auto synched_task = task_sync.synchronized([] { mambatests::fail_now(); });
task_sync.join_tasks();
synched_task(); // noop
}
@ -141,13 +123,13 @@ namespace mamba
{
sequence.push_back('A');
task_started = true;
wait_condition([&] { return task_continue.load(); });
mambatests::wait_condition([&] { return task_continue.load(); });
sequence.push_back('F');
}
)
);
wait_condition([&] { return task_started.load(); });
mambatests::wait_condition([&] { return task_started.load(); });
REQUIRE(sequence == "A");
auto ft_unlocker = std::async(
@ -157,7 +139,7 @@ namespace mamba
{
sequence.push_back('B');
unlocker_ready = true;
wait_condition([&] { return unlocker_start.load(); });
mambatests::wait_condition([&] { return unlocker_start.load(); });
sequence.push_back('D');
std::this_thread::sleep_for(unlock_duration); // Make sure the time is long
// enough for joining to
@ -168,7 +150,7 @@ namespace mamba
)
);
wait_condition([&] { return unlocker_ready.load(); });
mambatests::wait_condition([&] { return unlocker_ready.load(); });
REQUIRE(sequence == "AB");
sequence.push_back('C');

View File

@ -0,0 +1,416 @@
// Copyright (c) 2025, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <atomic>
#include <concepts>
#include <future>
#include <memory>
#include <mutex>
#include <ranges>
#include <shared_mutex>
#include <thread>
#include <catch2/catch_all.hpp>
#include "mamba/util/synchronized_value.hpp"
#include "mambatests_utils.hpp"
namespace mamba::util
{
static_assert(BasicLockable<std::mutex>);
static_assert(BasicLockable<std::recursive_mutex>);
static_assert(BasicLockable<std::shared_mutex>);
static_assert(Lockable<std::mutex>);
static_assert(Lockable<std::recursive_mutex>);
static_assert(Lockable<std::shared_mutex>);
static_assert(Mutex<std::mutex>);
static_assert(Mutex<std::recursive_mutex>);
static_assert(Mutex<std::shared_mutex>);
static_assert(SharedMutex<std::shared_mutex>);
// Scope locked must be possible to move in different scopes without unlocking-relocking,
// so it is imperative that they are moveable, but should not be copyable.
static_assert(std::move_constructible<scoped_locked_ptr<std::unique_ptr<int>, std::mutex, true>>);
static_assert(std::move_constructible<
scoped_locked_ptr<std::unique_ptr<int>, std::recursive_mutex, true>>);
static_assert(std::move_constructible<scoped_locked_ptr<std::unique_ptr<int>, std::shared_mutex, true>>);
static_assert(std::move_constructible<scoped_locked_ptr<std::unique_ptr<int>, std::mutex, false>>);
static_assert(std::move_constructible<
scoped_locked_ptr<std::unique_ptr<int>, std::recursive_mutex, false>>);
static_assert(std::move_constructible<
scoped_locked_ptr<std::unique_ptr<int>, std::shared_mutex, false>>);
static_assert(std::is_nothrow_move_constructible_v<
scoped_locked_ptr<std::unique_ptr<int>, std::mutex, true>>);
static_assert(std::is_nothrow_move_constructible_v<
scoped_locked_ptr<std::unique_ptr<int>, std::recursive_mutex, true>>);
static_assert(std::is_nothrow_move_constructible_v<
scoped_locked_ptr<std::unique_ptr<int>, std::shared_mutex, true>>);
static_assert(std::is_nothrow_move_constructible_v<
scoped_locked_ptr<std::unique_ptr<int>, std::mutex, false>>);
static_assert(std::is_nothrow_move_constructible_v<
scoped_locked_ptr<std::unique_ptr<int>, std::recursive_mutex, false>>);
static_assert(std::is_nothrow_move_constructible_v<
scoped_locked_ptr<std::unique_ptr<int>, std::shared_mutex, false>>);
static_assert(std::is_nothrow_move_assignable_v<
scoped_locked_ptr<std::unique_ptr<int>, std::mutex, true>>);
static_assert(std::is_nothrow_move_assignable_v<
scoped_locked_ptr<std::unique_ptr<int>, std::recursive_mutex, true>>);
static_assert(std::is_nothrow_move_assignable_v<
scoped_locked_ptr<std::unique_ptr<int>, std::shared_mutex, true>>);
static_assert(std::is_nothrow_move_assignable_v<
scoped_locked_ptr<std::unique_ptr<int>, std::mutex, false>>);
static_assert(std::is_nothrow_move_assignable_v<
scoped_locked_ptr<std::unique_ptr<int>, std::recursive_mutex, false>>);
static_assert(std::is_nothrow_move_assignable_v<
scoped_locked_ptr<std::unique_ptr<int>, std::shared_mutex, false>>);
}
namespace
{
struct ValueType
{
int x = 0;
auto increment() -> void
{
x++;
}
auto next_value() const -> ValueType
{
return { x + 1 };
}
auto operator<=>(const ValueType&) const noexcept = default;
};
using supported_mutex_types = std::tuple<std::mutex, std::shared_mutex, std::recursive_mutex>;
TEMPLATE_LIST_TEST_CASE("synchronized_value basics", "[template][thread-safe]", supported_mutex_types)
{
using synchronized_value = mamba::util::synchronized_value<ValueType, TestType>;
SECTION("default constructible")
{
synchronized_value a;
}
static constexpr auto initial_value = ValueType{ 42 };
synchronized_value sv{ initial_value };
SECTION("value access and assignation")
{
REQUIRE(sv.unsafe_get() == initial_value);
REQUIRE(sv.value() == initial_value);
REQUIRE(sv == initial_value);
REQUIRE(sv->x == initial_value.x);
const auto& const_sv = std::as_const(sv);
REQUIRE(const_sv.unsafe_get() == initial_value);
REQUIRE(const_sv.value() == initial_value);
REQUIRE(const_sv == initial_value);
REQUIRE(const_sv->x == initial_value.x);
sv->increment();
const auto expected_new_value = initial_value.next_value();
REQUIRE(sv.unsafe_get() == expected_new_value);
REQUIRE(sv.value() == expected_new_value);
REQUIRE(sv == expected_new_value);
REQUIRE(sv != initial_value);
REQUIRE(sv->x == expected_new_value.x);
REQUIRE(const_sv.unsafe_get() == expected_new_value);
REQUIRE(const_sv.value() == expected_new_value);
REQUIRE(const_sv == expected_new_value);
REQUIRE(const_sv != initial_value);
REQUIRE(const_sv->x == expected_new_value.x);
sv = initial_value;
REQUIRE(sv.unsafe_get() == initial_value);
REQUIRE(sv.value() == initial_value);
REQUIRE(sv == initial_value);
REQUIRE(sv != expected_new_value);
REQUIRE(sv->x == initial_value.x);
REQUIRE(const_sv.unsafe_get() == initial_value);
REQUIRE(const_sv.value() == initial_value);
REQUIRE(const_sv == initial_value);
REQUIRE(const_sv != expected_new_value);
REQUIRE(const_sv->x == initial_value.x);
}
SECTION("value access using synchronize")
{
sv = initial_value;
{
auto sync_sv = std::as_const(sv).synchronize();
REQUIRE(*sync_sv == initial_value);
REQUIRE(sync_sv->x == initial_value.x);
}
REQUIRE(sv.unsafe_get() == initial_value);
REQUIRE(sv.value() == initial_value);
REQUIRE(sv == initial_value);
REQUIRE(sv->x == initial_value.x);
static constexpr auto expected_value = ValueType{ 12345 };
{
auto sync_sv = sv.synchronize();
sync_sv->x = expected_value.x;
}
REQUIRE(sv.unsafe_get() == expected_value);
REQUIRE(sv.value() == expected_value);
REQUIRE(sv == expected_value);
REQUIRE(sv->x == expected_value.x);
{
auto sync_sv = sv.synchronize();
*sync_sv = initial_value;
}
REQUIRE(sv.unsafe_get() == initial_value);
REQUIRE(sv.value() == initial_value);
REQUIRE(sv == initial_value);
REQUIRE(sv->x == initial_value.x);
}
SECTION("value access using apply")
{
sv = initial_value;
{
auto result = std::as_const(sv).apply([](const ValueType& value) { return value.x; });
REQUIRE(result == initial_value.x);
}
REQUIRE(sv.unsafe_get() == initial_value);
REQUIRE(sv.value() == initial_value);
REQUIRE(sv == initial_value);
REQUIRE(sv->x == initial_value.x);
static constexpr auto expected_value = ValueType{ 98765 };
sv.apply([](ValueType& value) { value = expected_value; });
REQUIRE(sv.unsafe_get() == expected_value);
REQUIRE(sv.value() == expected_value);
REQUIRE(sv == expected_value);
REQUIRE(sv->x == expected_value.x);
sv.apply([](ValueType& value, auto new_value) { value = new_value; }, initial_value);
REQUIRE(sv.unsafe_get() == initial_value);
REQUIRE(sv.value() == initial_value);
REQUIRE(sv == initial_value);
REQUIRE(sv->x == initial_value.x);
}
}
TEMPLATE_LIST_TEST_CASE(
"synchronized_value initializer-list",
"[template][thread-safe]",
supported_mutex_types
)
{
using synchronized_value = mamba::util::synchronized_value<std::vector<int>, TestType>;
synchronized_value values{ 1, 2, 3, 4 };
}
TEMPLATE_LIST_TEST_CASE("synchronized_value apply example", "[template][thread-safe]", supported_mutex_types)
{
using synchronized_value = mamba::util::synchronized_value<std::vector<int>, TestType>;
const std::vector initial_values{ 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
const std::vector sorted_values{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
synchronized_value values{ initial_values };
values.apply(std::ranges::sort);
REQUIRE(values == sorted_values);
values.apply(std::ranges::sort, std::ranges::greater{});
REQUIRE(values == initial_values);
}
template <mamba::util::Mutex M>
auto test_concurrent_increment(
std::invocable<mamba::util::synchronized_value<ValueType, M>&> auto increment_task
)
{
static constexpr auto arbitrary_number_of_executing_threads = 512;
mamba::util::synchronized_value<ValueType, M> current_value;
static constexpr int expected_result = arbitrary_number_of_executing_threads;
std::atomic<bool> run_tasks = false; // used to launch tasks about the same time, simpler
// than condition_variable
std::vector<std::future<void>> tasks;
// Launch the reading and writing tasks (maybe threads, depends on how async is implemented)
for (int i = 0; i < expected_result * 2; ++i)
{
if (i % 2) // intertwine reading and writing tasks
{
// add writing task
tasks.push_back(std::async(
std::launch::async,
[&, increment_task]
{
// don't actually run until we get the green light
mambatests::wait_condition([&] { return run_tasks == true; });
increment_task(current_value);
}
));
}
else
{
// add reading task
tasks.push_back(std::async(
std::launch::async,
[&]
{
// don't actually run until we get the green light
mambatests::wait_condition([&] { return run_tasks == true; });
const auto& readonly_value = std::as_const(current_value);
static constexpr auto arbitrary_read_count = 100;
long long sum = 0;
for (int c = 0; c < arbitrary_read_count; ++c)
{
sum += readonly_value->x; // TODO: also try to mix reading and writing
// using different kinds of access
std::this_thread::yield(); // for timing randomness and limit
// over-exhaustion
}
REQUIRE(sum != 0); // It is possible but extremely unlikely that all
// reading tasks will read before any writing tasks.
}
));
}
}
run_tasks = true; // green light, tasks will run probably concurrently, worse case in
// unpredictable order
for (auto& task : tasks)
{
task.wait(); // wait all to be finished
}
REQUIRE(current_value->x == expected_result);
}
TEMPLATE_LIST_TEST_CASE(
"synchronized_value thread-safe direct_access",
"[template][thread-safe]",
supported_mutex_types
)
{
using synchronized_value = mamba::util::synchronized_value<ValueType, TestType>;
test_concurrent_increment<TestType>([](synchronized_value& sv) { sv->x += 1; });
}
TEMPLATE_LIST_TEST_CASE(
"synchronized_value thread-safe synchronize",
"[template][thread-safe]",
supported_mutex_types
)
{
using synchronized_value = mamba::util::synchronized_value<ValueType, TestType>;
test_concurrent_increment<TestType>(
[](synchronized_value& sv)
{
auto synched_sv = sv.synchronize();
synched_sv->x += 1;
}
);
}
TEMPLATE_LIST_TEST_CASE(
"synchronized_value thread-safe apply",
"[template][thread-safe]",
supported_mutex_types
)
{
using synchronized_value = mamba::util::synchronized_value<ValueType, TestType>;
test_concurrent_increment<TestType>([](synchronized_value& sv)
{ sv.apply([](ValueType& value) { value.x += 1; }); });
}
TEMPLATE_LIST_TEST_CASE(
"synchronized_value thread-safe multiple synchronize",
"[template][thread-safe]",
supported_mutex_types
)
{
using synchronized_value = mamba::util::synchronized_value<ValueType, TestType>;
const mamba::util::synchronized_value<std::vector<int>, std::shared_mutex> extra_values{ 1 };
test_concurrent_increment<TestType>(
[&](synchronized_value& sv)
{
auto [ssv, sev] = synchronize(sv, extra_values);
ssv->x += sev->front();
}
);
}
TEST_CASE("synchronized_value basics multiple synchronize")
{
using namespace mamba::util;
// mutables
synchronized_value<ValueType> a{ ValueType{ 1 } };
synchronized_value<ValueType, std::recursive_mutex> b{ ValueType{ 3 } };
synchronized_value<ValueType, std::shared_mutex> c{ ValueType{ 5 } };
synchronized_value<std::vector<int>> d{ 7 };
synchronized_value<std::vector<int>, std::recursive_mutex> e{ 9 };
synchronized_value<std::vector<int>, std::shared_mutex> f{ 11 };
// immutables (readonly)
const synchronized_value<ValueType> ca{ ValueType{ 2 } };
const synchronized_value<ValueType, std::recursive_mutex> cb{ ValueType{ 4 } };
const synchronized_value<ValueType, std::shared_mutex> cc{ ValueType{ 6 } };
const synchronized_value<std::vector<int>> cd{ 8 };
const synchronized_value<std::vector<int>, std::recursive_mutex> ce{ 10 };
const synchronized_value<std::vector<int>, std::shared_mutex> cf{ 12 };
std::vector<int> values;
{
auto [sa, sca, sb, scb, sc, scc, sd, scd, se, sce, sf, scf] = mamba::util::
synchronize(a, ca, b, cb, c, cc, d, cd, e, ce, f, cf);
static_assert(std::same_as<decltype(sa), scoped_locked_ptr<ValueType, std::mutex, false>>);
static_assert(std::same_as<decltype(sca), scoped_locked_ptr<ValueType, std::mutex, true>>);
static_assert(std::same_as<decltype(sb), scoped_locked_ptr<ValueType, std::recursive_mutex, false>>);
static_assert(std::same_as<decltype(scb), scoped_locked_ptr<ValueType, std::recursive_mutex, true>>);
static_assert(std::same_as<decltype(sc), scoped_locked_ptr<ValueType, std::shared_mutex, false>>);
static_assert(std::same_as<decltype(scc), scoped_locked_ptr<ValueType, std::shared_mutex, true>>);
static_assert(std::same_as<decltype(sd), scoped_locked_ptr<std::vector<int>, std::mutex, false>>);
static_assert(std::same_as<decltype(scd), scoped_locked_ptr<std::vector<int>, std::mutex, true>>);
static_assert(std::same_as<
decltype(se),
scoped_locked_ptr<std::vector<int>, std::recursive_mutex, false>>);
static_assert(std::same_as<
decltype(sce),
scoped_locked_ptr<std::vector<int>, std::recursive_mutex, true>>);
static_assert(std::same_as<
decltype(sf),
scoped_locked_ptr<std::vector<int>, std::shared_mutex, false>>);
static_assert(std::same_as<
decltype(scf),
scoped_locked_ptr<std::vector<int>, std::shared_mutex, true>>);
values.push_back(sa->x);
values.push_back(sca->x);
values.push_back(sb->x);
values.push_back(scb->x);
values.push_back(sc->x);
values.push_back(scc->x);
values.push_back(sd->front());
values.push_back(scd->front());
values.push_back(se->front());
values.push_back(sce->front());
values.push_back(sf->front());
values.push_back(scf->front());
}
std::ranges::sort(values);
REQUIRE(values == std::vector{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 });
}
}