Skip to content

Commit 2d28292

Browse files
authored
Merge pull request #227 from konard/issue-210-5e73c01c0c16
feat: std::mutex в pam_pmm_state для потокобезопасной инициализации (Этап 12.1, Проблема 3 Этап C, Issue #210)
2 parents bc757d2 + c45b02f commit 2d28292

5 files changed

Lines changed: 176 additions & 10 deletions

File tree

pam_pmm.h

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "pstringview_pmm.h"
2424
#include <cstdio>
2525
#include <cstring>
26+
#include <mutex>
2627
#include <type_traits>
2728

2829
namespace pjson
@@ -170,14 +171,20 @@ static_assert( std::is_trivially_copyable<pam_pmm_registry>::value,
170171
* Этап B (Issue #209): все pam_pmm_* функции принимают pam_pmm_state&
171172
* как явный параметр. Глобальные обёртки без параметра сохранены для
172173
* обратной совместимости и делегируют глобальному синглтону.
174+
*
175+
* Этап C (Issue #210): добавлен std::mutex для потокобезопасной
176+
* инициализации. Мьютекс защищает init/destroy/reset/save от
177+
* гонок при одновременном доступе из нескольких потоков.
173178
*/
174179
struct pam_pmm_state
175180
{
176181
char filename[256] = {}; ///< Имя файла хранилища
177182
uintptr_t root_offset = 0; ///< Смещение корневой структуры в ПАП
178183
bool initialized = false; ///< Флаг инициализации
184+
mutable std::mutex mtx; ///< Мьютекс для потокобезопасной инициализации (Этап C)
179185

180186
/// Сбросить все поля к начальным значениям.
187+
/// @note Вызывающий должен удерживать mtx (или гарантировать эксклюзивный доступ).
181188
void reset()
182189
{
183190
filename[0] = '\0';
@@ -330,6 +337,8 @@ inline uintptr_t pam_pmm_create_root_and_registry()
330337
*/
331338
inline void pam_pmm_init( pam_pmm_state& state, const char* filename )
332339
{
340+
std::lock_guard<std::mutex> lock( state.mtx );
341+
333342
// Сохраняем имя файла.
334343
if ( filename != nullptr )
335344
{
@@ -410,14 +419,25 @@ inline void pam_pmm_init( const char* filename )
410419
/**
411420
* @brief Сохранить PMM в файл (явное состояние).
412421
*/
413-
inline void pam_pmm_save( pam_pmm_state& state )
422+
/// Внутренняя реализация save без блокировки мьютекса.
423+
/// @note Вызывающий должен удерживать state.mtx.
424+
inline void pam_pmm_save_unlocked( pam_pmm_state& state )
414425
{
415426
if ( state.filename[0] == '\0' )
416427
return;
417428

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

432+
/**
433+
* @brief Сохранить PMM в файл (явное состояние).
434+
*/
435+
inline void pam_pmm_save( pam_pmm_state& state )
436+
{
437+
std::lock_guard<std::mutex> lock( state.mtx );
438+
pam_pmm_save_unlocked( state );
439+
}
440+
421441
/// Обёртка для обратной совместимости.
422442
inline void pam_pmm_save()
423443
{
@@ -431,7 +451,8 @@ inline void pam_pmm_save()
431451
*/
432452
inline void pam_pmm_destroy( pam_pmm_state& state )
433453
{
434-
pam_pmm_save( state );
454+
std::lock_guard<std::mutex> lock( state.mtx );
455+
pam_pmm_save_unlocked( state );
435456
PamManager::destroy();
436457
state.reset();
437458
}
@@ -449,6 +470,8 @@ inline void pam_pmm_destroy()
449470
*/
450471
inline void pam_pmm_reset( pam_pmm_state& state )
451472
{
473+
std::lock_guard<std::mutex> lock( state.mtx );
474+
452475
// Уничтожаем и создаём заново.
453476
PamManager::destroy();
454477
PamManager::create( PAM_PMM_INITIAL_SIZE );
@@ -470,6 +493,7 @@ inline void pam_pmm_reset()
470493
*/
471494
inline bool pam_pmm_is_initialized( const pam_pmm_state& state )
472495
{
496+
std::lock_guard<std::mutex> lock( state.mtx );
473497
return state.initialized && PamManager::is_initialized();
474498
}
475499

@@ -1074,6 +1098,7 @@ inline void pam_pmm_reserve_slots( uintptr_t /*min_slots*/ )
10741098
*/
10751099
inline bool pam_pmm_validate( const pam_pmm_state& state )
10761100
{
1101+
std::lock_guard<std::mutex> lock( state.mtx );
10771102
return state.initialized && PamManager::is_initialized();
10781103
}
10791104

plan.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
| 7. Унификация итераторов | CRTP-база pjson_iterator_base + шаблонный pjson_range; ~22 строки удалено ||
2222

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

2626
---
2727

@@ -39,14 +39,14 @@
3939

4040
---
4141

42-
### ~~Проблема 3: Глобальное состояние в pam_pmm.h~~ (Этап A ✅)
42+
### ~~Проблема 3: Глобальное состояние в pam_pmm.h~~
4343

4444
**Решено (Этап A) в Этапе 10.1:** Три разрозненные статические переменные (`filename`, `root_offset`, `initialized`) инкапсулированы в структуру `pam_pmm_state` с методом `reset()`. Глобальный синглтон `pam_pmm_global_state()` заменяет прямой доступ к переменным. Функции `detail::` делегируют синглтону для обратной совместимости. Добавлены 4 теста.
4545

46-
**Остающиеся этапы (будущие задачи):**
46+
**Все этапы выполнены:**
4747
1. ~~**Этап A:** Инкапсулировать три переменные в структуру `pam_pmm_state`~~
4848
2. ~~**Этап B:** Передавать `pam_pmm_state&` как явный параметр вместо обращения к глобальным переменным~~
49-
3. **Этап C:** Опционально — защита `std::mutex` для потокобезопасной инициализации
49+
3. ~~**Этап C:** Защита `std::mutex` для потокобезопасной инициализации~~
5050

5151
---
5252

@@ -181,7 +181,7 @@ pvector был бы предпочтительнее **только** при ч
181181

182182
| # | Проблема | Файл | Сложность | Влияние |
183183
|---|----------|------|-----------|---------|
184-
| ~~3~~ | ~~Глобальное состояние PMM (Этап A)~~ | ~~pam_pmm.h~~ | ~~Высокая~~ ||
184+
| ~~3~~ | ~~Глобальное состояние PMM (Этапы A+B+C)~~ | ~~pam_pmm.h~~ | ~~Высокая~~ ||
185185
| ~~7~~ | ~~Нет escaping '/' в путях~~ | ~~pjson_db_pmm.h~~ | ~~Средняя~~ ||
186186
| ~~10~~ | ~~Многократный resolve в is_*()~~ | ~~pjson_node.h~~ | ~~Средняя~~ ||
187187
| ~~11~~ | ~~const-корректность _walk_path~~ | ~~pjson_db_pmm.h~~ | ~~Средняя~~ ||
@@ -213,6 +213,10 @@ pvector был бы предпочтительнее **только** при ч
213213
214214
Этап 11: Проблема 3 — Этап B (явный параметр состояния)
215215
11.1 ✅ pam_pmm_state& как явный параметр всех pam_pmm_* функций; pjson_db_pmm хранит ссылку
216+
217+
Этап 12: Проблема 3 — Этап C (потокобезопасная инициализация)
218+
12.1 ✅ std::mutex в pam_pmm_state; lock_guard в init/destroy/reset/save/is_initialized/validate
219+
→ тесты: все 715 тестов проходят (6 новых тестов на потокобезопасность)
216220
```
217221

218222
---
@@ -221,6 +225,7 @@ pvector был бы предпочтительнее **только** при ч
221225

222226
| Дата | Изменение |
223227
|------|-----------|
228+
| 2026-03-22 | Этап 12.1: std::mutex в pam_pmm_state для потокобезопасной инициализации (Issue #210) |
224229
| 2026-03-22 | Этап 11.1: pam_pmm_state& как явный параметр pam_pmm_* функций; pjson_db_pmm хранит ссылку на состояние (Issue #209) |
225230
| 2026-03-22 | Этап 10.4: const-корректность _walk_path — разделение на _walk_path_read (const) и _walk_path_create (Issue #208) |
226231
| 2026-03-22 | Этап 10.3: оптимизация tag-проверок на горячих путях — сокращение избыточных pmm_resolve (Issue #207) |

readme.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ int main() {
139139
| `pjson_db_pmm.h` | D | Менеджер персистной JSON-БД: path-адресация, `put`/`get`/`erase`, `$ref`, метрики, поиск, клонирование |
140140
| `deps/pmm/pmm.h` | A | [PersistMemoryManager](https://github.com/netkeep80/PersistMemoryManager) — бэкенд ПАП |
141141
| `main.cpp` || Демонстрационная программа |
142-
| `tests/` || Тесты на Catch2 (709 тестов, ~360 000 assertion) |
142+
| `tests/` || Тесты на Catch2 (715 тестов, ~360 000 assertion) |
143143
| `CMakeLists.txt` || Система сборки (CMake 3.16+, C++20) |
144144

145145
---
@@ -451,12 +451,12 @@ db.put("/copy/name", "Bob");
451451

452452
## Известные ограничения
453453

454-
- **Глобальное состояние PMM** — в одном процессе может быть открыта только одна БД (см. [plan.md](plan.md), Проблема 3); состояние инкапсулировано в `pam_pmm_state` (Этап A), все `pam_pmm_*` функции принимают `pam_pmm_state&` как явный параметр (Этап B), `pjson_db_pmm` хранит ссылку на состояние; PamManager остаётся глобальным синглтоном
454+
- **Глобальное состояние 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 остаётся глобальным синглтоном
455455
- ~~**Нет escaping `/` в путях**~~**Исправлено** в Этапе 10.2: поддержка RFC 6901 (JSON Pointer) — `~1` для `/`, `~0` для `~` в сегментах путей
456456
- ~~**Утечка временных узлов метрик**~~**Исправлено** в Этапе 8.4: один pre-allocated узел переиспользуется для всех вызовов метрик
457457
- ~~**Многократный resolve в is_*() проверках**~~**Исправлено** в Этапе 10.3: `is_number()`, `deref()`, traversal и walk_path оптимизированы для единственного `pmm_resolve` вместо повторных вызовов
458458
- ~~**const-некорректность _walk_path**~~**Исправлено** в Этапе 10.4: шаблонный `_walk_path<bool>() const` разделён на `_walk_path_read() const` (только чтение) и `_walk_path_create()` (мутирующий)
459-
- **Не потокобезопасно** — CacheManagerConfig (по умолчанию) использует NoLock; для многопоточности нужен PersistentDataConfig
459+
- **Частичная потокобезопасность**`pam_pmm_state` защищён `std::mutex` (init/destroy/reset/save); CacheManagerConfig (по умолчанию) использует NoLock для PMM-операций; для полной многопоточности нужен PersistentDataConfig
460460
- **Строки не освобождаются** — словарь `pstringview_pmm` только растёт
461461

462462
---

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ set(TEST_SOURCES
4141
test_pjson_rfc6901.cpp
4242
test_pjson_tag_opt.cpp
4343
test_pjson_const_correctness.cpp
44+
test_pam_pmm_threadsafe.cpp
4445
)
4546

4647
add_executable(tests ${TEST_SOURCES})

tests/test_pam_pmm_threadsafe.cpp

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* @file test_pam_pmm_threadsafe.cpp
3+
* @brief Тесты потокобезопасности pam_pmm_state (Этап C, Issue #210).
4+
*
5+
* Проверяют, что std::mutex в pam_pmm_state защищает init/destroy/reset/save
6+
* от гонок при одновременном доступе из нескольких потоков.
7+
*/
8+
9+
#include <catch2/catch_test_macros.hpp>
10+
#include <atomic>
11+
#include <thread>
12+
#include <vector>
13+
14+
#include "pam_pmm.h"
15+
16+
using namespace pjson;
17+
18+
// ═══════════════════════════════════════════════════════════════════════════
19+
// БАЗОВЫЕ ТЕСТЫ МЬЮТЕКСА В PAM_PMM_STATE
20+
// ═══════════════════════════════════════════════════════════════════════════
21+
22+
TEST_CASE( "pam_pmm_threadsafe: mutex exists in pam_pmm_state", "[pam_pmm][threadsafe]" )
23+
{
24+
// Проверяем, что pam_pmm_state содержит мьютекс (компилируется).
25+
pam_pmm_state state;
26+
std::lock_guard<std::mutex> lock( state.mtx );
27+
REQUIRE_FALSE( state.initialized );
28+
}
29+
30+
TEST_CASE( "pam_pmm_threadsafe: concurrent is_initialized reads", "[pam_pmm][threadsafe]" )
31+
{
32+
pam_pmm_init( nullptr );
33+
REQUIRE( pam_pmm_is_initialized() );
34+
35+
constexpr int NUM_THREADS = 8;
36+
std::atomic<int> success_count{ 0 };
37+
std::vector<std::thread> threads;
38+
39+
for ( int i = 0; i < NUM_THREADS; ++i )
40+
{
41+
threads.emplace_back(
42+
[&success_count]()
43+
{
44+
if ( pam_pmm_is_initialized() )
45+
success_count.fetch_add( 1 );
46+
} );
47+
}
48+
49+
for ( auto& t : threads )
50+
t.join();
51+
52+
REQUIRE( success_count.load() == NUM_THREADS );
53+
54+
pam_pmm_destroy();
55+
}
56+
57+
TEST_CASE( "pam_pmm_threadsafe: init and destroy are serialized", "[pam_pmm][threadsafe]" )
58+
{
59+
// Проверяем, что последовательные init/destroy не приводят к гонкам.
60+
constexpr int ITERATIONS = 10;
61+
62+
for ( int i = 0; i < ITERATIONS; ++i )
63+
{
64+
pam_pmm_init( nullptr );
65+
REQUIRE( pam_pmm_is_initialized() );
66+
pam_pmm_destroy();
67+
REQUIRE_FALSE( pam_pmm_is_initialized() );
68+
}
69+
}
70+
71+
TEST_CASE( "pam_pmm_threadsafe: concurrent init/destroy with explicit state", "[pam_pmm][threadsafe]" )
72+
{
73+
// Используем явное состояние для проверки, что мьютекс защищает
74+
// одновременный доступ к одному и тому же state из разных потоков.
75+
pam_pmm_state state;
76+
77+
constexpr int ITERATIONS = 5;
78+
constexpr int NUM_THREADS = 4;
79+
80+
for ( int iter = 0; iter < ITERATIONS; ++iter )
81+
{
82+
// Инициализируем.
83+
pam_pmm_init( state, nullptr );
84+
REQUIRE( pam_pmm_is_initialized( state ) );
85+
86+
// Несколько потоков одновременно проверяют состояние.
87+
std::atomic<int> check_count{ 0 };
88+
std::vector<std::thread> threads;
89+
90+
for ( int i = 0; i < NUM_THREADS; ++i )
91+
{
92+
threads.emplace_back(
93+
[&state, &check_count]()
94+
{
95+
// Каждый поток проверяет is_initialized через мьютекс.
96+
bool ok = pam_pmm_is_initialized( state );
97+
if ( ok )
98+
check_count.fetch_add( 1 );
99+
} );
100+
}
101+
102+
for ( auto& t : threads )
103+
t.join();
104+
105+
REQUIRE( check_count.load() == NUM_THREADS );
106+
107+
// Уничтожаем.
108+
pam_pmm_destroy( state );
109+
REQUIRE_FALSE( pam_pmm_is_initialized( state ) );
110+
}
111+
}
112+
113+
TEST_CASE( "pam_pmm_threadsafe: reset is serialized", "[pam_pmm][threadsafe]" )
114+
{
115+
pam_pmm_init( nullptr );
116+
117+
// Создаём объект.
118+
auto& state = pam_pmm_global_state();
119+
uintptr_t off = pam_pmm_create<int>( state, "test_threadsafe" );
120+
REQUIRE( off != 0 );
121+
122+
// Reset через мьютекс.
123+
pam_pmm_reset( state );
124+
REQUIRE( pam_pmm_is_initialized( state ) );
125+
REQUIRE( pam_pmm_find( state, "test_threadsafe" ) == 0 );
126+
127+
pam_pmm_destroy( state );
128+
}
129+
130+
TEST_CASE( "pam_pmm_threadsafe: pam_pmm_state is not copyable due to mutex", "[pam_pmm][threadsafe]" )
131+
{
132+
// std::mutex делает pam_pmm_state некопируемым — это ожидаемое поведение.
133+
STATIC_REQUIRE_FALSE( std::is_copy_constructible<pam_pmm_state>::value );
134+
STATIC_REQUIRE_FALSE( std::is_copy_assignable<pam_pmm_state>::value );
135+
}

0 commit comments

Comments
 (0)