Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions pam_pmm.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "pstringview_pmm.h"
#include <cstdio>
#include <cstring>
#include <mutex>
#include <type_traits>

namespace pjson
Expand Down Expand Up @@ -170,14 +171,20 @@ static_assert( std::is_trivially_copyable<pam_pmm_registry>::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';
Expand Down Expand Up @@ -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<std::mutex> lock( state.mtx );

// Сохраняем имя файла.
if ( filename != nullptr )
{
Expand Down Expand Up @@ -410,14 +419,25 @@ 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;

pmm::save_manager<PamManager>( state.filename );
}

/**
* @brief Сохранить PMM в файл (явное состояние).
*/
inline void pam_pmm_save( pam_pmm_state& state )
{
std::lock_guard<std::mutex> lock( state.mtx );
pam_pmm_save_unlocked( state );
}

/// Обёртка для обратной совместимости.
inline void pam_pmm_save()
{
Expand All @@ -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<std::mutex> lock( state.mtx );
pam_pmm_save_unlocked( state );
PamManager::destroy();
state.reset();
}
Expand All @@ -449,6 +470,8 @@ inline void pam_pmm_destroy()
*/
inline void pam_pmm_reset( pam_pmm_state& state )
{
std::lock_guard<std::mutex> lock( state.mtx );

// Уничтожаем и создаём заново.
PamManager::destroy();
PamManager::create( PAM_PMM_INITIAL_SIZE );
Expand All @@ -470,6 +493,7 @@ inline void pam_pmm_reset()
*/
inline bool pam_pmm_is_initialized( const pam_pmm_state& state )
{
std::lock_guard<std::mutex> lock( state.mtx );
return state.initialized && PamManager::is_initialized();
}

Expand Down Expand Up @@ -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<std::mutex> lock( state.mtx );
return state.initialized && PamManager::is_initialized();
}

Expand Down
15 changes: 10 additions & 5 deletions plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
| 7. Унификация итераторов | CRTP-база pjson_iterator_base + шаблонный pjson_range; ~22 строки удалено | ✅ |

**Итого удалено:** ~5 файлов (~1900 строк), ~381 строка дублирования.
**Тесты:** 709 тестов, ~360 000 assertion.
**Тесты:** 715 тестов, ~360 000 assertion.

---

Expand All @@ -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` для потокобезопасной инициализации~~ ✅

---

Expand Down Expand Up @@ -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~~ | ~~Средняя~~ | ✅ |
Expand Down Expand Up @@ -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 новых тестов на потокобезопасность)
```

---
Expand All @@ -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) |
Expand Down
6 changes: 3 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |

---
Expand Down Expand Up @@ -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<bool>() 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` только растёт

---
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
135 changes: 135 additions & 0 deletions tests/test_pam_pmm_threadsafe.cpp
Original file line number Diff line number Diff line change
@@ -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 <catch2/catch_test_macros.hpp>
#include <atomic>
#include <thread>
#include <vector>

#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<std::mutex> 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<int> success_count{ 0 };
std::vector<std::thread> 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<int> check_count{ 0 };
std::vector<std::thread> 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<int>( 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<pam_pmm_state>::value );
STATIC_REQUIRE_FALSE( std::is_copy_assignable<pam_pmm_state>::value );
}
Loading