Коротко — код вызывает неопределённое поведение: одновременные вызовы `v.push_back` не потокобезопасны (гонки данных, одновременная реаллокация буфера, повреждение памяти, invalidation итераторов и т.п.). Ниже — проблемы, способы исправления и альтернативы. Проблемы - Доступ без синхронизации к общему состоянию `std::vector` → data race → undefined behavior. - Одновременная реаллокация внутреннего буфера при наполнении → повреждение памяти. - Итераторы/указатели вектора могут быть инвалидаированы при реаллокации. Как исправить (варианты) 1) Простейший — мьютекс - Гарантирует корректность, но вызывает блокировку при каждой вставке. Пример: std::mutex m; std::thread t1([&]{ for(int i=0;i<100010001000;++i){ std::lock_guard g(m); v.push_back(i); } }); std::thread t2([&]{ for(int i=0;i<100010001000;++i){ std::lock_guard g(m); v.push_back(i); } }); 2) Зарезервировать место + атомарный индекс (без блокировок на вставке) - Выделить память заранее: `v.resize(200020002000);` или `v.reserve(200020002000);` и затем писать в разные ячейки по атомарному индексу. Пример (без realoc, безопасно): v.resize(200020002000); std::atomic idx{0}; std::thread t1([&]{ for(int i=0;i<100010001000;++i){ size_t pos = idx.fetch_add(1); v[pos] = i; } }); ... // t2 аналогично 3) Разделить диапазоны заранее (deterministic partitioning) - Если заранее известно, что каждый поток запишет ровно NNN элементов, сделать `v.resize(num_threads * N)` и дать каждому потоку свой диапазон `v[offset .. offset+N-1]`. Без синхронизации. 4) Буферы на поток + объединение - Каждый поток собирает в свой `std::vector`, затем один поток (или в защищённом участке) сливает результаты в общий вектор: thread-local v1, v2; потом { std::lock_guard g(m); v.insert(v.end(), v1.begin(), v1.end()); v.insert(v.end(), v2.begin(), v2.end()); } Альтернативы блокировкам (lock-free / concurrent контейнеры) - Использовать специализированные многопоточные контейнеры: Intel TBB `tbb::concurrent_vector`, moodycamel::ConcurrentQueue, folly::AtomicUnboundedQueue и т.п. Они реализуют безопасную параллельную вставку. - lock-free структуры (кольцевые буферы с атомарным tail/head) — подходит, если API позволяет (FIFO очередь и т.д.). - Использовать std::atomic и предвыделение/индексацию (см. пункт 2) — простая lock-free стратегия для однонаправленной записи. Замечания - Если используете `reserve` (а не `resize`), нельзя писать через `operator[]` за пределами существующих элементов — нужно `resize` или аккуратно управлять "занятостью" символов. - При использовании lock-free решений учитывать требования к порядку элементов и исключениям при аллокации. - Выбор зависит от требований: простота → мьютекс; производительность и меньшая блокировка → thread-local + merge или предвыделение + atomic index; готовые контейнеры → TBB/folly/моodycamel. Конкретный быстрый рецепт для данного примера: - Самый простой корректный: взять `std::mutex` вокруг `push_back`. - Более эффективный: `v.resize(200020002000); std::atomic idx{0};` и записывать по `v[idx.fetch_add(1)]`.
Проблемы
- Доступ без синхронизации к общему состоянию `std::vector` → data race → undefined behavior.
- Одновременная реаллокация внутреннего буфера при наполнении → повреждение памяти.
- Итераторы/указатели вектора могут быть инвалидаированы при реаллокации.
Как исправить (варианты)
1) Простейший — мьютекс
- Гарантирует корректность, но вызывает блокировку при каждой вставке.
Пример:
std::mutex m;
std::thread t1([&]{ for(int i=0;i<100010001000;++i){ std::lock_guard g(m); v.push_back(i); } });
std::thread t2([&]{ for(int i=0;i<100010001000;++i){ std::lock_guard g(m); v.push_back(i); } });
2) Зарезервировать место + атомарный индекс (без блокировок на вставке)
- Выделить память заранее: `v.resize(200020002000);` или `v.reserve(200020002000);` и затем писать в разные ячейки по атомарному индексу.
Пример (без realoc, безопасно):
v.resize(200020002000);
std::atomic idx{0};
std::thread t1([&]{ for(int i=0;i<100010001000;++i){ size_t pos = idx.fetch_add(1); v[pos] = i; } });
... // t2 аналогично
3) Разделить диапазоны заранее (deterministic partitioning)
- Если заранее известно, что каждый поток запишет ровно NNN элементов, сделать `v.resize(num_threads * N)` и дать каждому потоку свой диапазон `v[offset .. offset+N-1]`. Без синхронизации.
4) Буферы на поток + объединение
- Каждый поток собирает в свой `std::vector`, затем один поток (или в защищённом участке) сливает результаты в общий вектор:
thread-local v1, v2; потом
{
std::lock_guard g(m);
v.insert(v.end(), v1.begin(), v1.end());
v.insert(v.end(), v2.begin(), v2.end());
}
Альтернативы блокировкам (lock-free / concurrent контейнеры)
- Использовать специализированные многопоточные контейнеры: Intel TBB `tbb::concurrent_vector`, moodycamel::ConcurrentQueue, folly::AtomicUnboundedQueue и т.п. Они реализуют безопасную параллельную вставку.
- lock-free структуры (кольцевые буферы с атомарным tail/head) — подходит, если API позволяет (FIFO очередь и т.д.).
- Использовать std::atomic и предвыделение/индексацию (см. пункт 2) — простая lock-free стратегия для однонаправленной записи.
Замечания
- Если используете `reserve` (а не `resize`), нельзя писать через `operator[]` за пределами существующих элементов — нужно `resize` или аккуратно управлять "занятостью" символов.
- При использовании lock-free решений учитывать требования к порядку элементов и исключениям при аллокации.
- Выбор зависит от требований: простота → мьютекс; производительность и меньшая блокировка → thread-local + merge или предвыделение + atomic index; готовые контейнеры → TBB/folly/моodycamel.
Конкретный быстрый рецепт для данного примера:
- Самый простой корректный: взять `std::mutex` вокруг `push_back`.
- Более эффективный: `v.resize(200020002000); std::atomic idx{0};` и записывать по `v[idx.fetch_add(1)]`.