mirror of https://github.com/mamba-org/mamba.git
`synchronized_value` (#3984)
This commit is contained in:
parent
ffd08b8661
commit
98b48779e8
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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');
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue