diff --git a/README.md b/README.md index 898f2ee..bf1f502 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ This preserves normal C++ member semantics, keeps raw object access available wh `Type_Descriptor` describes the **type**. `Property_Object` adds state and behavior to an **instance**. -The schema contains the registered property tuple, object defaults and the default synchronization plan. A `Property_Object` resolves that plan for each instance and owns its lock topology and mutex storage. +The schema contains the registered property tuple, object defaults and the default synchronization plan. `Property_Object` shares one resolved default lock topology per type and keeps only instance synchronization state that is actually required: real mutex storage for locking policies and a compact override layout only when an instance explicitly supplies `Property_Synchronization`. This distinction matters for performance and architecture: structural description is type-level information; managed synchronization is instance-level state. diff --git a/README.zh-CN.md b/README.zh-CN.md index b6a5f7e..d003f74 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -121,7 +121,7 @@ struct Device : Property_Object { `Type_Descriptor` 描述的是**类型**;`Property_Object` 管理的是**实例**。 -Schema 保存注册属性列表、对象默认 Attribute 和默认同步计划。`Property_Object` 在每个实例上解析同步计划,并持有自己的锁拓扑和 mutex 存储。 +Schema 保存注册属性列表、对象默认 Attribute 和默认同步计划。`Property_Object` 对同一类型共享一份解析后的默认 lock topology;实例只保留真正需要的同步状态:真实锁策略需要 mutex storage,只有显式传入 `Property_Synchronization` 的实例才额外持有紧凑的覆盖布局。 这个边界必须长期保持:结构描述属于类型级;同步状态属于实例级。 diff --git a/core/include/structive/property/property_object.hpp b/core/include/structive/property/property_object.hpp index 3111758..9e8aaa5 100644 --- a/core/include/structive/property/property_object.hpp +++ b/core/include/structive/property/property_object.hpp @@ -42,6 +42,43 @@ template concept Synchronization_Policy = requires { typename Policy::mutex_type; } && Shared_Lockable; +namespace detail { +template > +class Mutex_Storage; +template +class Mutex_Storage { + std::unique_ptr mutexes_; +public: + Mutex_Storage() = default; + explicit Mutex_Storage(std::size_t count) : mutexes_(count ? std::make_unique(count) : nullptr) {} + void reset(std::size_t count) { + mutexes_ = count ? std::make_unique(count) : nullptr; + } + Mutex& get(std::size_t index) noexcept { + return mutexes_[index]; + } + Mutex& get(std::size_t index) const noexcept { + return mutexes_[index]; + } +}; +template +class Mutex_Storage { + static Mutex& mutex() noexcept { + static Mutex value; + return value; + } +public: + Mutex_Storage() = default; + explicit Mutex_Storage(std::size_t) {} + void reset(std::size_t) {} + Mutex& get(std::size_t) noexcept { + return mutex(); + } + Mutex& get(std::size_t) const noexcept { + return mutex(); + } +}; +} enum class Managed_Access_Mode { internal, external, @@ -120,6 +157,8 @@ public: using object_type = Derived; using mutex_type = typename Lock_Policy::mutex_type; private: + static constexpr bool uses_real_mutexes = !std::same_as; + struct Empty_Guard_Locks {}; struct Dynamic_Lock_Targets { std::vector slots; std::vector unsynchronized_properties; @@ -131,23 +170,43 @@ private: std::array unsynchronized_properties{}; std::size_t unsynchronized_count{}; }; - std::vector lock_slots_; - std::size_t lock_count_{}; - std::unique_ptr locks_; + std::unique_ptr custom_lock_layout_; + [[no_unique_address]] detail::Mutex_Storage mutex_storage_; + static const auto& default_resolved_synchronization() { + static const auto resolved = resolve_synchronization_plan(type_descriptor(), type_descriptor().synchronization_plan()); + return resolved; + } template void initialize(const Schema& schema, const Synchronization_Plan& plan) { auto resolved = resolve_synchronization_plan(schema, plan); - lock_slots_.assign(resolved.lock_slots.begin(), resolved.lock_slots.end()); - lock_count_ = resolved.lock_count; - locks_ = lock_count_ ? std::make_unique(lock_count_) : nullptr; + auto layout = std::make_unique(Schema::property_count + 1); + layout[0] = resolved.lock_count; + std::copy(resolved.lock_slots.begin(), resolved.lock_slots.end(), layout.get() + 1); + custom_lock_layout_ = std::move(layout); + mutex_storage_.reset(resolved.lock_count); } void copy_synchronization_from(const Property_Object& other) { - lock_slots_ = other.lock_slots_; - lock_count_ = other.lock_count_; - locks_ = lock_count_ ? std::make_unique(lock_count_) : nullptr; + using Schema = type_descriptor_schema_t; + if (other.custom_lock_layout_) { + auto layout = std::make_unique(Schema::property_count + 1); + std::copy_n(other.custom_lock_layout_.get(), Schema::property_count + 1, layout.get()); + custom_lock_layout_ = std::move(layout); + } else { + custom_lock_layout_.reset(); + } + mutex_storage_.reset(lock_count()); + } + std::size_t lock_count() const noexcept { + return custom_lock_layout_ ? custom_lock_layout_[0] : default_resolved_synchronization().lock_count; } std::size_t slot(std::size_t index) const noexcept { - return lock_slots_[index]; + return custom_lock_layout_ ? custom_lock_layout_[index + 1] : default_resolved_synchronization().slot(index); + } + mutex_type& mutex(std::size_t index) noexcept { + return mutex_storage_.get(index); + } + mutex_type& mutex(std::size_t index) const noexcept { + return mutex_storage_.get(index); } template static void sort_unique_prefix(std::array& values, std::size_t& count) { @@ -303,25 +362,36 @@ private: auto read_one() const { using Schema = type_descriptor_schema_t; static_assert(property_read_allowed_v); - using Value = typename Schema::template property_type::value_type; + using Property = typename Schema::template property_type; + using Value = typename Property::value_type; + if constexpr (!uses_real_mutexes && !Property::accessor_type::synchronized_view_read) { + return Value(read_unlocked(*this)); + } auto lock_slot = slot(Index); Single_Read_View view{*this, Index, lock_slot}; + if constexpr (!uses_real_mutexes) { + return Value(read_unlocked(view)); + } if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { return Value(read_unlocked(view)); } - std::shared_lock lock{locks_[lock_slot]}; + std::shared_lock lock{mutex(lock_slot)}; return Value(read_unlocked(view)); } template void write_one(Value&& value) { using Schema = type_descriptor_schema_t; static_assert(property_write_allowed_v); + if constexpr (!uses_real_mutexes) { + write_unlocked(std::forward(value)); + return; + } auto lock_slot = slot(Index); if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { write_unlocked(std::forward(value)); return; } - std::unique_lock lock{locks_[lock_slot]}; + std::unique_lock lock{mutex(lock_slot)}; write_unlocked(std::forward(value)); } public: @@ -330,7 +400,7 @@ public: const Property_Object* owner_{}; std::vector slots_; std::vector unsynchronized_properties_; - std::vector> locks_; + [[no_unique_address]] std::conditional_t>, Empty_Guard_Locks> locks_; bool holds(std::size_t index) const { auto lock_slot = owner_->slot(index); if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { @@ -340,9 +410,11 @@ public: } friend class Property_Object; Read_Guard(const Property_Object& owner, Dynamic_Lock_Targets targets) : owner_(&owner), slots_(std::move(targets.slots)), unsynchronized_properties_(std::move(targets.unsynchronized_properties)) { - locks_.reserve(slots_.size()); - for (auto lock_slot : slots_) { - locks_.emplace_back(owner_->locks_[lock_slot]); + if constexpr (uses_real_mutexes) { + locks_.reserve(slots_.size()); + for (auto lock_slot : slots_) { + locks_.emplace_back(owner_->mutex(lock_slot)); + } } } public: @@ -376,7 +448,7 @@ public: Property_Object* owner_{}; std::vector slots_; std::vector unsynchronized_properties_; - std::vector> locks_; + [[no_unique_address]] std::conditional_t>, Empty_Guard_Locks> locks_; bool holds(std::size_t index) const { auto lock_slot = owner_->slot(index); if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { @@ -386,9 +458,11 @@ public: } friend class Property_Object; Write_Guard(Property_Object& owner, Dynamic_Lock_Targets targets) : owner_(&owner), slots_(std::move(targets.slots)), unsynchronized_properties_(std::move(targets.unsynchronized_properties)) { - locks_.reserve(slots_.size()); - for (auto lock_slot : slots_) { - locks_.emplace_back(owner_->locks_[lock_slot]); + if constexpr (uses_real_mutexes) { + locks_.reserve(slots_.size()); + for (auto lock_slot : slots_) { + locks_.emplace_back(owner_->mutex(lock_slot)); + } } } public: @@ -445,7 +519,7 @@ public: class Static_Read_Guard { const Property_Object* owner_{}; Static_Lock_Targets targets_; - std::array, Capacity> locks_{}; + [[no_unique_address]] std::conditional_t, Capacity>, Empty_Guard_Locks> locks_{}; bool holds(std::size_t index) const { auto lock_slot = owner_->slot(index); if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { @@ -455,8 +529,10 @@ public: } friend class Property_Object; Static_Read_Guard(const Property_Object& owner, Static_Lock_Targets targets) : owner_(&owner), targets_(std::move(targets)) { - for (std::size_t index = 0; index < targets_.slot_count; ++index) { - locks_[index] = std::shared_lock{owner_->locks_[targets_.slots[index]]}; + if constexpr (uses_real_mutexes) { + for (std::size_t index = 0; index < targets_.slot_count; ++index) { + locks_[index] = std::shared_lock{owner_->mutex(targets_.slots[index])}; + } } } public: @@ -489,7 +565,7 @@ public: class Static_Write_Guard { Property_Object* owner_{}; Static_Lock_Targets targets_; - std::array, Capacity> locks_{}; + [[no_unique_address]] std::conditional_t, Capacity>, Empty_Guard_Locks> locks_{}; bool holds(std::size_t index) const { auto lock_slot = owner_->slot(index); if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { @@ -499,8 +575,10 @@ public: } friend class Property_Object; Static_Write_Guard(Property_Object& owner, Static_Lock_Targets targets) : owner_(&owner), targets_(std::move(targets)) { - for (std::size_t index = 0; index < targets_.slot_count; ++index) { - locks_[index] = std::unique_lock{owner_->locks_[targets_.slots[index]]}; + if constexpr (uses_real_mutexes) { + for (std::size_t index = 0; index < targets_.slot_count; ++index) { + locks_[index] = std::unique_lock{owner_->mutex(targets_.slots[index])}; + } } } public: @@ -617,10 +695,12 @@ private: callback(context, property_index, key, typeid(Value), std::addressof(value)); } }; - if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { + if constexpr (!uses_real_mutexes) { + emit(); + } else if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { emit(); } else { - std::shared_lock lock{locks_[lock_slot]}; + std::shared_lock lock{mutex(lock_slot)}; emit(); } result = Runtime_Access_Result::ok; @@ -688,10 +768,7 @@ private: return &value; } protected: - Property_Object() : Property_Object_Base(runtime_interface()) { - const auto& schema = type_descriptor(); - initialize(schema, schema.synchronization_plan()); - } + Property_Object() : Property_Object_Base(runtime_interface()), mutex_storage_(default_resolved_synchronization().lock_count) {} explicit Property_Object(Property_Synchronization synchronization) : Property_Object_Base(runtime_interface()) { initialize(type_descriptor(), synchronization.plan); } @@ -713,7 +790,12 @@ public: return type_descriptor(); } Resolved_Synchronization_View resolved_synchronization() const noexcept { - return {lock_slots_, lock_count_}; + using Schema = type_descriptor_schema_t; + if (custom_lock_layout_) { + return {{custom_lock_layout_.get() + 1, Schema::property_count}, custom_lock_layout_[0]}; + } + const auto& resolved = default_resolved_synchronization(); + return {resolved.lock_slots, resolved.lock_count}; } Derived& unsafe_object() noexcept { return static_cast(*this); diff --git a/core/tests/property_core_test.cpp b/core/tests/property_core_test.cpp index a23280f..bc099cb 100644 --- a/core/tests/property_core_test.cpp +++ b/core/tests/property_core_test.cpp @@ -74,6 +74,25 @@ struct structive::Type_Descriptor { ); } }; +struct Lockless_Device : Property_Object { + Lockless_Device() = default; + explicit Lockless_Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {} + int left{1}; + int right{2}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + synchronization(sync_all_independent, sync_group("sum", "left", "right", "sum")), + field < &Lockless_Device::left > (key < "left" >), + field < &Lockless_Device::right > (key < "right" >), + computed_property([](const auto& view) { + return view.template get<&Lockless_Device::left>() + view.template get<&Lockless_Device::right>(); + }, key < "sum" >) + ); + } +}; template concept Has_Write_Temperature = requires(View view) { view.template write<&Device::temperature>(1); @@ -124,6 +143,14 @@ int main() { REQUIRE(shared_device.lock_slot<&Device::pressure>() == shared_device.lock_slot<&Device::min_speed>()); Device grouped_device{property_synchronization(synchronization(sync_all_independent, sync_group < &Device::temperature, &Device::pressure > ("environment")))}; REQUIRE(grouped_device.lock_slot<&Device::temperature>() == grouped_device.lock_slot<&Device::pressure>()); + Device shared_copy{shared_device}; + REQUIRE(shared_copy.lock_slot<&Device::temperature>() == shared_copy.lock_slot<&Device::pressure>()); + Device shared_move{std::move(shared_copy)}; + REQUIRE(shared_move.lock_slot<&Device::temperature>() == shared_move.lock_slot<&Device::pressure>()); + Device assignment_target; + auto assignment_target_slot = assignment_target.lock_slot<&Device::pressure>(); + assignment_target = shared_device; + REQUIRE(assignment_target.lock_slot<&Device::pressure>() == assignment_target_slot); device.external().write < &Device::temperature > (30); REQUIRE(device.external().read<&Device::temperature>() == 30); device.persistence().load < &Device::pressure > (101); @@ -151,6 +178,13 @@ int main() { REQUIRE(computed.external().read_key<"speed_span">() == 90); computed.write < &Computed_Device::min_speed > (20); REQUIRE(computed.external().read_key<"speed_span">() == 80); + Lockless_Device lockless; + REQUIRE(lockless.read<&Lockless_Device::left>() == 1); + lockless.write < &Lockless_Device::right > (4); + REQUIRE(lockless.read_key<"sum">() == 5); + auto lockless_guard = lockless.lock_shared<&Lockless_Device::left, &Lockless_Device::right>(); + REQUIRE(lockless_guard.get<&Lockless_Device::left>() == 1); + REQUIRE(lockless_guard.get<&Lockless_Device::right>() == 4); Non_Copyable_Device non_copyable; bool non_copyable_visited = false; non_copyable.external().for_each_readable_locked([&](auto, const auto&, const auto& value) { diff --git a/docs/CORE_GUIDE.md b/docs/CORE_GUIDE.md index f7a2f0a..377c2ec 100644 --- a/docs/CORE_GUIDE.md +++ b/docs/CORE_GUIDE.md @@ -407,7 +407,7 @@ auto policy = property_synchronization( ); ``` -The schema remains the same; only the instance lock topology changes. +The schema remains the same; only that instance uses an override lock topology. Default instances share one resolved topology per type, so the default plan is not re-resolved for every object. ## 15. Inspect resolved synchronization @@ -628,7 +628,7 @@ struct Device : Property_Object { }; ``` -`No_Lock_Policy` uses `Null_Shared_Mutex`; it removes real mutual exclusion and should only be selected when external ownership guarantees make that correct. +`No_Lock_Policy` uses `Null_Shared_Mutex`; it removes real mutual exclusion and should only be selected when external ownership guarantees make that correct. Its default managed-object path stores no mutex array and performs no per-instance heap allocation for synchronization. A custom `Property_Synchronization` may still allocate a compact override topology because computed-property synchronization-domain checks must preserve the selected per-instance layout. ## 23. Raw object access diff --git a/docs/CORE_GUIDE.zh-CN.md b/docs/CORE_GUIDE.zh-CN.md index 7e9e350..b68eb83 100644 --- a/docs/CORE_GUIDE.zh-CN.md +++ b/docs/CORE_GUIDE.zh-CN.md @@ -407,7 +407,7 @@ auto policy = property_synchronization( ); ``` -Schema 本身不变,只改变该实例的 lock topology。 +Schema 本身不变,只有这个实例使用覆盖后的 lock topology。默认实例按类型共享一份解析结果,因此不会为每个对象重复解析默认同步计划。 ## 15. 查看解析后的同步拓扑 @@ -628,7 +628,7 @@ struct Device : Property_Object { }; ``` -`No_Lock_Policy` 使用 `Null_Shared_Mutex`,它取消真实互斥;只有外部所有权规则能够保证正确性时才应该使用。 +`No_Lock_Policy` 使用 `Null_Shared_Mutex`,它取消真实互斥;只有外部所有权规则能够保证正确性时才应该使用。默认 managed-object 路径不保存 mutex 数组,也不会为同步状态产生每实例堆分配。显式使用自定义 `Property_Synchronization` 时仍可能分配一份紧凑覆盖 topology,因为 computed property 的同步域检查必须保留该实例选择的布局。 ## 23. Raw Object Access diff --git a/docs/DESIGN.md b/docs/DESIGN.md index d3d742f..3fcd3a4 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -72,8 +72,9 @@ This is the structural definition of the type. `Property_Object` provides: -- resolved lock-slot topology; -- per-instance mutex storage; +- a type-shared resolved default lock-slot topology; +- per-instance mutex storage only when the selected lock policy requires real mutexes; +- a compact per-instance topology only for explicit synchronization overrides; - managed typed reads and writes; - capability views; - static and runtime multi-property guards; @@ -84,7 +85,7 @@ This is instance behavior, not schema identity. ### 3.3 Design rule -Do not move instance state into the schema, and do not make schema metadata depend on one particular instance-management policy. +Do not move mutable instance synchronization state into the schema, and do not make schema metadata depend on one particular instance-management policy. Immutable topology derived from the type-level default plan may be shared across all instances of that type. A future user may want to describe a large number of plain objects without paying per-object synchronization cost. The architecture should continue to leave that possibility open. diff --git a/docs/DESIGN.zh-CN.md b/docs/DESIGN.zh-CN.md index f9bc691..b0b66b3 100644 --- a/docs/DESIGN.zh-CN.md +++ b/docs/DESIGN.zh-CN.md @@ -72,8 +72,9 @@ Structive 把类型级描述和实例级管理分开。 `Property_Object` 提供: -- 解析后的 lock slot 拓扑; -- 每实例 mutex 存储; +- 同一类型共享的默认解析 lock slot 拓扑; +- 只有真实锁策略才需要的每实例 mutex 存储; +- 只有显式同步覆盖实例才持有的紧凑 topology; - managed typed read/write; - capability view; - 静态与运行时多属性 guard; @@ -84,7 +85,7 @@ Structive 把类型级描述和实例级管理分开。 ### 3.3 设计约束 -不要把实例状态塞进 Schema,也不要让 Schema 必须依赖某一种特定的实例管理策略。 +不要把可变的实例同步状态塞进 Schema,也不要让 Schema 必须依赖某一种特定的实例管理策略。由类型级默认同步计划推导出的不可变 topology 可以由同类型所有实例共享。 以后完全可能有用户只想描述大量普通对象,却不愿意为每个对象承担同步状态成本。当前架构应该持续保留这种可能性。