Проанализируйте следующий фрагмент на C++: vector v; thread t1([&]{ for(int i=0;i<100;i++) v.push_back(i); }); thread t2([&]{ for(int i=0;i

24 Ноя в 12:26
2 +2
0
Ответы
1
Коротко — исходный фрагмент небезопасен: одновременные вызовы `v.push_back` из двух потоков приводят к гонкам данных и к неопределённому поведению (возможна порча памяти, краш, некорректные значения).
Почему:
- `std::vector::push_back` изменяет внутреннее состояние контейнера (size, возможно capacity, пишет элементы). Если два потока одновременно пишут без синхронизации, это — data race, а по модели памяти C++ любое несовместимое параллельное обращение (по крайней мере одно — запись) без синхронизации даёт undefined behavior.
- При необходимости realocation (увеличение capacity) указатели/буферы переаллочиваются и чужие записи могут писать в освобождённую/неправильную память.
Возможные решения (корректные и эффективные), с замечаниями:
1) Простая и корректная синхронизация — мьютекс
- Обычный, очевидный способ:
- использовать `std::mutex m;` и защищать `v.push_back(...)` через `std::lock_guard`.
- Плюс: простота и корректность.
- Минус: сильная сериализация операций push_back, возможный узкий горлышко при высокой конкуренции.
2) Предвыделить память и писать в непересекающиеся диапазоны (без блокировок)
- В main: `v.resize(200200200);` (или `reserve(200200200)` + правильная инициализация размера — см. ниже).
- Пусть t1 пишет `v[i] = i` для \(i=0..\,\(99\)\), t2 пишет `v[i+100] = i+100` для \(i=0..\,\(99\)\`.
- Инвариант: нет пересечения индексов, не вызывается push_back/reallocation, каждая ячейка доступна и модифицируется одним потоком ⇒ безопасно без дополнительных примитивов.
- Важно: `reserve` сам по себе НЕ увеличивает size, поэтому запись по индексу за пределами size — UB. Нужна именно `resize` или управление size через atomics+поэлементную запись в заранее выделенную память.
3) Аллокатор индексов через atomic + предвыделение хранилища
- Сделать `v.resize(200200200); std::atomic idx{0};`
- Каждый поток делает `int pos = idx.fetch_add(1, std::memory_order_relaxed); v[pos] = value;`
- Гарантирует уникальные позиции; подходит если порядок вставки не важен.
- Нужна предварительная `resize` чтобы избежать UB.
4) Потокозависимые буферы (рекомендуется для производительности)
- Каждый поток собирает свои значения в локальный `std::vector local; local.reserve(100100100);` затем после join один поток сливает все локальные в основной под одной блокировкой или при заранее `reserve(200200200)` выполняет последовательные `insert`/`memcpy`.
- Очень эффективно: минимальная синхронизация, высокая локальность кеша.
5) Использовать готовые конкурентные контейнеры
- Например `tbb::concurrent_vector`, `concurrent_queue` из Intel TBB, структуры из Folly/Boost.Lockfree.
- Удобно, когда нужна масштабируемая неблокирующая вставка.
Замечания по памяти и порядкам (memory model):
- Любая корректная синхронизация должна обеспечивать happens‑before между модификацией контейнера и чтением из него (например, join уже даёт happens‑before: всё, что сделал поток, видимо после join).
- При использовании `std::atomic::fetch_add` можно применять `memory_order_relaxed`, если требуется только уникальная позиция и видимость выполняется через другие синхронизации (join/барьеры). Но будьте внимательны: семантика видимости — проектируйте правильно.
Резюме — простое решение: либо защита `push_back` мьютексом, либо (обычно эффективнее) заранее `v.resize(200200200)` и назначение непересекающихся диапазонов каждому потоку или сбор в локальные буферы и последующее объединение. Для высокопроизводительных случаев рассматривайте специализированные concurrent-контейнеры.
24 Ноя в 13:17
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир