Дан фрагмент кода на C++ с утечкой памяти и условной гонкой в многопоточном режиме (опишите небольшой пример с new/delete и std::thread): выявите ошибки, объясните, как их отловить инструментами (valgrind, sanitizer'ы, thread sanitizer) и предложите безопасную переработку кода
Ниже — компактный пример, который демонстрирует одновременно утечку памяти (new без delete) и условную гонку (data race) при параллельном доступе к общему ресурсу.
Баговый пример (bug.cpp)
#include <iostream> #include <thread> #include <vector> struct Data { int counter; }; int main() { Data* d = new Data(); // выделили, но дальше не удаляем -> утечка d->counter = 0; std::vector<std::thread> threads; for (int i = 0; i < 4; ++i) { threads.emplace_back([d]() { // поток захватывает указатель for (int j = 0; j < 100000; ++j) { d->counter++; // НЕзащищённый доступ -> data race } }); } for (auto &t : threads) t.join(); std::cout << "counter = " << d->counter << std::endl; // missing delete d; <-- утечка памяти return 0; }
Ошибки
Утечка памяти: выделили d через new, но не вызвали delete — Valgrind/LeakSanitizer сообщат "definitely lost".Условная гонка: несколько потоков одновременно изменяют d->counter без синхронизации — это data race (undefined behavior). ThreadSanitizer обнаружит её.
Как отловить инструментами
1) ThreadSanitizer (TSan) — для data race:
Компиляция (g++/clang++): g++ -std=c++17 -O1 -g -fsanitize=thread -pthread bug.cpp -o bug-tsanЗапуск: ./bug-tsanОжидаемый вывод: TSan напишет WARNING: ThreadSanitizer: data race и покажет стек вызовов, где происходит конкурентный доступ.
Примечание: TSan специально для гонок; он не сообщает об утечках памяти.
2) AddressSanitizer / LeakSanitizer — для поиска утечек:
Компиляция: g++ -std=c++17 -O1 -g -fsanitize=address -fno-omit-frame-pointer -pthread bug.cpp -o bug-asanЗапуск: ASAN_OPTIONS=detect_leaks=1 ./bug-asanОжидаемый вывод: LeakSanitizer сообщит о "LEAK" / "definitely lost" и укажет место new в стеке.
3) Valgrind (классика для утечек, но медленнее):
Сборка без sanitizers: g++ -std=c++17 -O0 -g -pthread bug.cpp -o bugЗапуск: valgrind --leak-check=full --show-leak-kinds=all ./bugВалгринд покажет количество "definitely lost" байт и стек-след к месту выделения.
Замечание о совместимости: ASan и TSan обычно нельзя включать одновременно (конфликты). Запускайте их по отдельности.
Безопасная переработка кода
Рекомендации:
Не использовать сырой new/delete — применять умные указатели (std::unique_ptr или std::shared_ptr) для автоматического освобождения.Для совместного доступа использовать std::atomic или синхронизацию (std::mutex), в зависимости от семантики.Запускать тесты с sanitizers и/или valgrind в CI.
Исправленный вариант 1 (используем std::shared_ptr + std::atomic):
#include <iostream> #include <thread> #include <vector> #include <memory> #include <atomic> struct Data { std::atomic<int> counter{0}; }; int main() { auto d = std::make_shared<Data>(); // автоматическое управление памятью std::vector<std::thread> threads; for (int i = 0; i < 4; ++i) { threads.emplace_back([d]() { // копируем shared_ptr, безопасно передаём владение for (int j = 0; j < 100000; ++j) { d->counter.fetch_add(1, std::memory_order_relaxed); // атомарно } }); } for (auto &t : threads) t.join(); std::cout << "counter = " << d->counter.load() << std::endl; // d автоматически удалится, когда последний shared_ptr выйдет из области видимости return 0; }
Почему этот вариант безопасен:
Нет утечки — shared_ptr освободит память.Нет data race — counter — std::atomic, атомарные операции безопасны между потоками.
Исправленный вариант 2 (std::unique_ptr + mutex):
#include <iostream> #include <thread> #include <vector> #include <memory> #include <mutex> struct Data { int counter = 0; }; int main() { auto d = std::make_unique<Data>(); std::mutex mtx; std::vector<std::thread> threads; for (int i = 0; i < 4; ++i) { threads.emplace_back([&d, &mtx]() { for (int j = 0; j < 100000; ++j) { std::lock_guard<std::mutex> lk(mtx); ++d->counter; // защита мьютексом } }); } for (auto &t : threads) t.join(); std::cout << "counter = " << d->counter << std::endl; return 0; }
Почему:
unique_ptr управляет временем жизни (нет утечки).мьютекс обеспечивает корректную последовательность доступа (нет гонки).
Дополнительные советы
Для простого счетчика атомики обычно предпочтительнее (меньше накладных расходов), для сложных структур — mutex или другие примитивы синхронизации.Запускайте sanitizers локально и в CI; TSan часто обнаруживает гонки, которые реже проявляются при обычных запусках.Valgrind хорош для поиска утечек в коде без sanitizers и выявления других проблем с памятью, но работает медленно.
Если хотите, могу:
Запустить (показать пример вывода) TSan/ASan/Valgrind на этом примере,Или переписать пример под конкретные требования (например, использование std::mutex, std::atomic, или pool-подход).
Ниже — компактный пример, который демонстрирует одновременно утечку памяти (new без delete) и условную гонку (data race) при параллельном доступе к общему ресурсу.
Баговый пример (bug.cpp)
#include <iostream>#include <thread>
#include <vector>
struct Data { int counter; };
int main() {
Data* d = new Data(); // выделили, но дальше не удаляем -> утечка
d->counter = 0;
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back([d]() { // поток захватывает указатель
for (int j = 0; j < 100000; ++j) {
d->counter++; // НЕзащищённый доступ -> data race
}
});
}
for (auto &t : threads) t.join();
std::cout << "counter = " << d->counter << std::endl;
// missing delete d; <-- утечка памяти
return 0;
}
Ошибки
Утечка памяти: выделили d через new, но не вызвали delete — Valgrind/LeakSanitizer сообщат "definitely lost".Условная гонка: несколько потоков одновременно изменяют d->counter без синхронизации — это data race (undefined behavior). ThreadSanitizer обнаружит её.Как отловить инструментами
1) ThreadSanitizer (TSan) — для data race:
Компиляция (g++/clang++):g++ -std=c++17 -O1 -g -fsanitize=thread -pthread bug.cpp -o bug-tsanЗапуск:
./bug-tsanОжидаемый вывод: TSan напишет WARNING: ThreadSanitizer: data race и покажет стек вызовов, где происходит конкурентный доступ.
Примечание: TSan специально для гонок; он не сообщает об утечках памяти.
2) AddressSanitizer / LeakSanitizer — для поиска утечек:
Компиляция:g++ -std=c++17 -O1 -g -fsanitize=address -fno-omit-frame-pointer -pthread bug.cpp -o bug-asanЗапуск:
ASAN_OPTIONS=detect_leaks=1 ./bug-asanОжидаемый вывод: LeakSanitizer сообщит о "LEAK" / "definitely lost" и укажет место new в стеке.
3) Valgrind (классика для утечек, но медленнее):
Сборка без sanitizers:g++ -std=c++17 -O0 -g -pthread bug.cpp -o bugЗапуск:
valgrind --leak-check=full --show-leak-kinds=all ./bugВалгринд покажет количество "definitely lost" байт и стек-след к месту выделения.
Замечание о совместимости: ASan и TSan обычно нельзя включать одновременно (конфликты). Запускайте их по отдельности.
Безопасная переработка кода
Рекомендации:
Не использовать сырой new/delete — применять умные указатели (std::unique_ptr или std::shared_ptr) для автоматического освобождения.Для совместного доступа использовать std::atomic или синхронизацию (std::mutex), в зависимости от семантики.Запускать тесты с sanitizers и/или valgrind в CI.Исправленный вариант 1 (используем std::shared_ptr + std::atomic):
#include <iostream>#include <thread>
#include <vector>
#include <memory>
#include <atomic>
struct Data { std::atomic<int> counter{0}; };
int main() {
auto d = std::make_shared<Data>(); // автоматическое управление памятью
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back([d]() { // копируем shared_ptr, безопасно передаём владение
for (int j = 0; j < 100000; ++j) {
d->counter.fetch_add(1, std::memory_order_relaxed); // атомарно
}
});
}
for (auto &t : threads) t.join();
std::cout << "counter = " << d->counter.load() << std::endl;
// d автоматически удалится, когда последний shared_ptr выйдет из области видимости
return 0;
}
Почему этот вариант безопасен:
Нет утечки — shared_ptr освободит память.Нет data race — counter — std::atomic, атомарные операции безопасны между потоками.Исправленный вариант 2 (std::unique_ptr + mutex):
#include <iostream>#include <thread>
#include <vector>
#include <memory>
#include <mutex>
struct Data { int counter = 0; };
int main() {
auto d = std::make_unique<Data>();
std::mutex mtx;
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back([&d, &mtx]() {
for (int j = 0; j < 100000; ++j) {
std::lock_guard<std::mutex> lk(mtx);
++d->counter; // защита мьютексом
}
});
}
for (auto &t : threads) t.join();
std::cout << "counter = " << d->counter << std::endl;
return 0;
}
Почему:
unique_ptr управляет временем жизни (нет утечки).мьютекс обеспечивает корректную последовательность доступа (нет гонки).Дополнительные советы
Для простого счетчика атомики обычно предпочтительнее (меньше накладных расходов), для сложных структур — mutex или другие примитивы синхронизации.Запускайте sanitizers локально и в CI; TSan часто обнаруживает гонки, которые реже проявляются при обычных запусках.Valgrind хорош для поиска утечек в коде без sanitizers и выявления других проблем с памятью, но работает медленно.Если хотите, могу:
Запустить (показать пример вывода) TSan/ASan/Valgrind на этом примере,Или переписать пример под конкретные требования (например, использование std::mutex, std::atomic, или pool-подход).