diff --git a/pam_pmm.h b/pam_pmm.h index a1d4252..bb660b7 100644 --- a/pam_pmm.h +++ b/pam_pmm.h @@ -23,6 +23,7 @@ #include "pstringview_pmm.h" #include #include +#include #include namespace pjson @@ -170,14 +171,20 @@ static_assert( std::is_trivially_copyable::value, * Этап B (Issue #209): все pam_pmm_* функции принимают pam_pmm_state& * как явный параметр. Глобальные обёртки без параметра сохранены для * обратной совместимости и делегируют глобальному синглтону. + * + * Этап C (Issue #210): добавлен std::mutex для потокобезопасной + * инициализации. Мьютекс защищает init/destroy/reset/save от + * гонок при одновременном доступе из нескольких потоков. */ struct pam_pmm_state { char filename[256] = {}; ///< Имя файла хранилища uintptr_t root_offset = 0; ///< Смещение корневой структуры в ПАП bool initialized = false; ///< Флаг инициализации + mutable std::mutex mtx; ///< Мьютекс для потокобезопасной инициализации (Этап C) /// Сбросить все поля к начальным значениям. + /// @note Вызывающий должен удерживать mtx (или гарантировать эксклюзивный доступ). void reset() { filename[0] = '\0'; @@ -330,6 +337,8 @@ inline uintptr_t pam_pmm_create_root_and_registry() */ inline void pam_pmm_init( pam_pmm_state& state, const char* filename ) { + std::lock_guard lock( state.mtx ); + // Сохраняем имя файла. if ( filename != nullptr ) { @@ -410,7 +419,9 @@ inline void pam_pmm_init( const char* filename ) /** * @brief Сохранить PMM в файл (явное состояние). */ -inline void pam_pmm_save( pam_pmm_state& state ) +/// Внутренняя реализация save без блокировки мьютекса. +/// @note Вызывающий должен удерживать state.mtx. +inline void pam_pmm_save_unlocked( pam_pmm_state& state ) { if ( state.filename[0] == '\0' ) return; @@ -418,6 +429,15 @@ inline void pam_pmm_save( pam_pmm_state& state ) pmm::save_manager( state.filename ); } +/** + * @brief Сохранить PMM в файл (явное состояние). + */ +inline void pam_pmm_save( pam_pmm_state& state ) +{ + std::lock_guard lock( state.mtx ); + pam_pmm_save_unlocked( state ); +} + /// Обёртка для обратной совместимости. inline void pam_pmm_save() { @@ -431,7 +451,8 @@ inline void pam_pmm_save() */ inline void pam_pmm_destroy( pam_pmm_state& state ) { - pam_pmm_save( state ); + std::lock_guard lock( state.mtx ); + pam_pmm_save_unlocked( state ); PamManager::destroy(); state.reset(); } @@ -449,6 +470,8 @@ inline void pam_pmm_destroy() */ inline void pam_pmm_reset( pam_pmm_state& state ) { + std::lock_guard lock( state.mtx ); + // Уничтожаем и создаём заново. PamManager::destroy(); PamManager::create( PAM_PMM_INITIAL_SIZE ); @@ -470,6 +493,7 @@ inline void pam_pmm_reset() */ inline bool pam_pmm_is_initialized( const pam_pmm_state& state ) { + std::lock_guard lock( state.mtx ); return state.initialized && PamManager::is_initialized(); } @@ -1074,6 +1098,7 @@ inline void pam_pmm_reserve_slots( uintptr_t /*min_slots*/ ) */ inline bool pam_pmm_validate( const pam_pmm_state& state ) { + std::lock_guard lock( state.mtx ); return state.initialized && PamManager::is_initialized(); } diff --git a/plan.md b/plan.md index 5c05200..fb069b7 100644 --- a/plan.md +++ b/plan.md @@ -21,7 +21,7 @@ | 7. Унификация итераторов | CRTP-база pjson_iterator_base + шаблонный pjson_range; ~22 строки удалено | ✅ | **Итого удалено:** ~5 файлов (~1900 строк), ~381 строка дублирования. -**Тесты:** 709 тестов, ~360 000 assertion. +**Тесты:** 715 тестов, ~360 000 assertion. --- @@ -39,14 +39,14 @@ --- -### ~~Проблема 3: Глобальное состояние в pam_pmm.h~~ (Этап A ✅) +### ~~Проблема 3: Глобальное состояние в pam_pmm.h~~ ✅ **Решено (Этап A) в Этапе 10.1:** Три разрозненные статические переменные (`filename`, `root_offset`, `initialized`) инкапсулированы в структуру `pam_pmm_state` с методом `reset()`. Глобальный синглтон `pam_pmm_global_state()` заменяет прямой доступ к переменным. Функции `detail::` делегируют синглтону для обратной совместимости. Добавлены 4 теста. -**Остающиеся этапы (будущие задачи):** +**Все этапы выполнены:** 1. ~~**Этап A:** Инкапсулировать три переменные в структуру `pam_pmm_state`~~ ✅ 2. ~~**Этап B:** Передавать `pam_pmm_state&` как явный параметр вместо обращения к глобальным переменным~~ ✅ -3. **Этап C:** Опционально — защита `std::mutex` для потокобезопасной инициализации +3. ~~**Этап C:** Защита `std::mutex` для потокобезопасной инициализации~~ ✅ --- @@ -181,7 +181,7 @@ pvector был бы предпочтительнее **только** при ч | # | Проблема | Файл | Сложность | Влияние | |---|----------|------|-----------|---------| -| ~~3~~ | ~~Глобальное состояние PMM (Этап A)~~ | ~~pam_pmm.h~~ | ~~Высокая~~ | ✅ | +| ~~3~~ | ~~Глобальное состояние PMM (Этапы A+B+C)~~ | ~~pam_pmm.h~~ | ~~Высокая~~ | ✅ | | ~~7~~ | ~~Нет escaping '/' в путях~~ | ~~pjson_db_pmm.h~~ | ~~Средняя~~ | ✅ | | ~~10~~ | ~~Многократный resolve в is_*()~~ | ~~pjson_node.h~~ | ~~Средняя~~ | ✅ | | ~~11~~ | ~~const-корректность _walk_path~~ | ~~pjson_db_pmm.h~~ | ~~Средняя~~ | ✅ | @@ -213,6 +213,10 @@ pvector был бы предпочтительнее **только** при ч Этап 11: Проблема 3 — Этап B (явный параметр состояния) 11.1 ✅ pam_pmm_state& как явный параметр всех pam_pmm_* функций; pjson_db_pmm хранит ссылку + +Этап 12: Проблема 3 — Этап C (потокобезопасная инициализация) + 12.1 ✅ std::mutex в pam_pmm_state; lock_guard в init/destroy/reset/save/is_initialized/validate + → тесты: все 715 тестов проходят (6 новых тестов на потокобезопасность) ``` --- @@ -221,6 +225,7 @@ pvector был бы предпочтительнее **только** при ч | Дата | Изменение | |------|-----------| +| 2026-03-22 | Этап 12.1: std::mutex в pam_pmm_state для потокобезопасной инициализации (Issue #210) | | 2026-03-22 | Этап 11.1: pam_pmm_state& как явный параметр pam_pmm_* функций; pjson_db_pmm хранит ссылку на состояние (Issue #209) | | 2026-03-22 | Этап 10.4: const-корректность _walk_path — разделение на _walk_path_read (const) и _walk_path_create (Issue #208) | | 2026-03-22 | Этап 10.3: оптимизация tag-проверок на горячих путях — сокращение избыточных pmm_resolve (Issue #207) | diff --git a/readme.md b/readme.md index 788cd5d..cf2281e 100644 --- a/readme.md +++ b/readme.md @@ -139,7 +139,7 @@ int main() { | `pjson_db_pmm.h` | D | Менеджер персистной JSON-БД: path-адресация, `put`/`get`/`erase`, `$ref`, метрики, поиск, клонирование | | `deps/pmm/pmm.h` | A | [PersistMemoryManager](https://github.com/netkeep80/PersistMemoryManager) — бэкенд ПАП | | `main.cpp` | — | Демонстрационная программа | -| `tests/` | — | Тесты на Catch2 (709 тестов, ~360 000 assertion) | +| `tests/` | — | Тесты на Catch2 (715 тестов, ~360 000 assertion) | | `CMakeLists.txt` | — | Система сборки (CMake 3.16+, C++20) | --- @@ -451,12 +451,12 @@ db.put("/copy/name", "Bob"); ## Известные ограничения -- **Глобальное состояние PMM** — в одном процессе может быть открыта только одна БД (см. [plan.md](plan.md), Проблема 3); состояние инкапсулировано в `pam_pmm_state` (Этап A), все `pam_pmm_*` функции принимают `pam_pmm_state&` как явный параметр (Этап B), `pjson_db_pmm` хранит ссылку на состояние; PamManager остаётся глобальным синглтоном +- **Глобальное состояние PMM** — в одном процессе может быть открыта только одна БД (см. [plan.md](plan.md), Проблема 3); состояние инкапсулировано в `pam_pmm_state` (Этап A), все `pam_pmm_*` функции принимают `pam_pmm_state&` как явный параметр (Этап B), `pjson_db_pmm` хранит ссылку на состояние; `std::mutex` в `pam_pmm_state` защищает init/destroy/reset/save от гонок (Этап C); PamManager остаётся глобальным синглтоном - ~~**Нет escaping `/` в путях**~~ — **Исправлено** в Этапе 10.2: поддержка RFC 6901 (JSON Pointer) — `~1` для `/`, `~0` для `~` в сегментах путей - ~~**Утечка временных узлов метрик**~~ — **Исправлено** в Этапе 8.4: один pre-allocated узел переиспользуется для всех вызовов метрик - ~~**Многократный resolve в is_*() проверках**~~ — **Исправлено** в Этапе 10.3: `is_number()`, `deref()`, traversal и walk_path оптимизированы для единственного `pmm_resolve` вместо повторных вызовов - ~~**const-некорректность _walk_path**~~ — **Исправлено** в Этапе 10.4: шаблонный `_walk_path() const` разделён на `_walk_path_read() const` (только чтение) и `_walk_path_create()` (мутирующий) -- **Не потокобезопасно** — CacheManagerConfig (по умолчанию) использует NoLock; для многопоточности нужен PersistentDataConfig +- **Частичная потокобезопасность** — `pam_pmm_state` защищён `std::mutex` (init/destroy/reset/save); CacheManagerConfig (по умолчанию) использует NoLock для PMM-операций; для полной многопоточности нужен PersistentDataConfig - **Строки не освобождаются** — словарь `pstringview_pmm` только растёт --- diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5fb44c0..fb093f4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -41,6 +41,7 @@ set(TEST_SOURCES test_pjson_rfc6901.cpp test_pjson_tag_opt.cpp test_pjson_const_correctness.cpp + test_pam_pmm_threadsafe.cpp ) add_executable(tests ${TEST_SOURCES}) diff --git a/tests/test_pam_pmm_threadsafe.cpp b/tests/test_pam_pmm_threadsafe.cpp new file mode 100644 index 0000000..f3b59da --- /dev/null +++ b/tests/test_pam_pmm_threadsafe.cpp @@ -0,0 +1,135 @@ +/** + * @file test_pam_pmm_threadsafe.cpp + * @brief Тесты потокобезопасности pam_pmm_state (Этап C, Issue #210). + * + * Проверяют, что std::mutex в pam_pmm_state защищает init/destroy/reset/save + * от гонок при одновременном доступе из нескольких потоков. + */ + +#include +#include +#include +#include + +#include "pam_pmm.h" + +using namespace pjson; + +// ═══════════════════════════════════════════════════════════════════════════ +// БАЗОВЫЕ ТЕСТЫ МЬЮТЕКСА В PAM_PMM_STATE +// ═══════════════════════════════════════════════════════════════════════════ + +TEST_CASE( "pam_pmm_threadsafe: mutex exists in pam_pmm_state", "[pam_pmm][threadsafe]" ) +{ + // Проверяем, что pam_pmm_state содержит мьютекс (компилируется). + pam_pmm_state state; + std::lock_guard lock( state.mtx ); + REQUIRE_FALSE( state.initialized ); +} + +TEST_CASE( "pam_pmm_threadsafe: concurrent is_initialized reads", "[pam_pmm][threadsafe]" ) +{ + pam_pmm_init( nullptr ); + REQUIRE( pam_pmm_is_initialized() ); + + constexpr int NUM_THREADS = 8; + std::atomic success_count{ 0 }; + std::vector threads; + + for ( int i = 0; i < NUM_THREADS; ++i ) + { + threads.emplace_back( + [&success_count]() + { + if ( pam_pmm_is_initialized() ) + success_count.fetch_add( 1 ); + } ); + } + + for ( auto& t : threads ) + t.join(); + + REQUIRE( success_count.load() == NUM_THREADS ); + + pam_pmm_destroy(); +} + +TEST_CASE( "pam_pmm_threadsafe: init and destroy are serialized", "[pam_pmm][threadsafe]" ) +{ + // Проверяем, что последовательные init/destroy не приводят к гонкам. + constexpr int ITERATIONS = 10; + + for ( int i = 0; i < ITERATIONS; ++i ) + { + pam_pmm_init( nullptr ); + REQUIRE( pam_pmm_is_initialized() ); + pam_pmm_destroy(); + REQUIRE_FALSE( pam_pmm_is_initialized() ); + } +} + +TEST_CASE( "pam_pmm_threadsafe: concurrent init/destroy with explicit state", "[pam_pmm][threadsafe]" ) +{ + // Используем явное состояние для проверки, что мьютекс защищает + // одновременный доступ к одному и тому же state из разных потоков. + pam_pmm_state state; + + constexpr int ITERATIONS = 5; + constexpr int NUM_THREADS = 4; + + for ( int iter = 0; iter < ITERATIONS; ++iter ) + { + // Инициализируем. + pam_pmm_init( state, nullptr ); + REQUIRE( pam_pmm_is_initialized( state ) ); + + // Несколько потоков одновременно проверяют состояние. + std::atomic check_count{ 0 }; + std::vector threads; + + for ( int i = 0; i < NUM_THREADS; ++i ) + { + threads.emplace_back( + [&state, &check_count]() + { + // Каждый поток проверяет is_initialized через мьютекс. + bool ok = pam_pmm_is_initialized( state ); + if ( ok ) + check_count.fetch_add( 1 ); + } ); + } + + for ( auto& t : threads ) + t.join(); + + REQUIRE( check_count.load() == NUM_THREADS ); + + // Уничтожаем. + pam_pmm_destroy( state ); + REQUIRE_FALSE( pam_pmm_is_initialized( state ) ); + } +} + +TEST_CASE( "pam_pmm_threadsafe: reset is serialized", "[pam_pmm][threadsafe]" ) +{ + pam_pmm_init( nullptr ); + + // Создаём объект. + auto& state = pam_pmm_global_state(); + uintptr_t off = pam_pmm_create( state, "test_threadsafe" ); + REQUIRE( off != 0 ); + + // Reset через мьютекс. + pam_pmm_reset( state ); + REQUIRE( pam_pmm_is_initialized( state ) ); + REQUIRE( pam_pmm_find( state, "test_threadsafe" ) == 0 ); + + pam_pmm_destroy( state ); +} + +TEST_CASE( "pam_pmm_threadsafe: pam_pmm_state is not copyable due to mutex", "[pam_pmm][threadsafe]" ) +{ + // std::mutex делает pam_pmm_state некопируемым — это ожидаемое поведение. + STATIC_REQUIRE_FALSE( std::is_copy_constructible::value ); + STATIC_REQUIRE_FALSE( std::is_copy_assignable::value ); +}