Неожиданная утечка памяти в многопоточной программе - PullRequest
1 голос
/ 07 февраля 2020

Я работаю над программой, использующей большое количество потоков, каждый поток выделяет в куче несколько мегабайт памяти. Когда эти потоки заканчиваются, большая часть оперативной памяти сохраняется программой.

Вот пример кода, выделяющего и освобождающего 1 МБ в 500 потоков, который показывает эту проблему:

#include <future>
#include <iostream>
#include <vector>

// filling a 1 MB array with 0
void task() {
    const size_t S = 1000000;
    int * tab = new int[S];
    std::fill(tab, tab + S, 0);
    delete[] tab;
}

int main() {
    std::vector<std::future<void>> threads;
    const size_t N = 500;

    std::this_thread::sleep_for(std::chrono::seconds(5));
    std::cout << "Starting threads" << std::endl;

    for (size_t i = 0 ; i < N ; ++i) {
        threads.push_back(std::async(std::launch::async, [=]() { return task(); }));
    }

    for (size_t i = 0 ; i < N ; ++i) {
        threads[i].get();
    }

    std::cout << "Threads ended" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(25));

    return 0;
}

На моем компьютере этот код, просто созданный с g++ -o exe main.cpp -lpthread, использует 1976 КБ перед сообщением «Запуск потоков» и 419 МБ после сообщения «Поток завершен». Эти значения являются просто примерами: когда я запускаю программу несколько раз, я могу получить разные значения.

Я пробовал valgrind / memcheck, но он не обнаруживает утечки.

У меня есть заметил, что блокировка операции «std :: fill» с помощью мьютекса, похоже, решает эту проблему (или в значительной степени уменьшает ее), но я не думаю, что это проблема состояния гонки, поскольку здесь нет общей памяти. Я полагаю, что мьютекс просто создает порядок выполнения между потоками, который избегает (или уменьшает) условия, при которых происходит утечка памяти.

Я использую Ubuntu 18.04, с G CC 7.4.0.

Спасибо за вашу помощь.

Аурелиен

Ответы [ 3 ]

3 голосов
/ 07 февраля 2020

Отсутствует утечка памяти, поскольку Valgrind / memcheck уже подтвердил вам.

[...] использует 1976 КБ до сообщения «Запуск потоков» и 419 МБ после сообщение "Темы закончились".

Две вещи:

  • В начале ваш вектор пуст.
  • В конце ваш вектор содержит 500 std::future<void> объектов.

Вот почему ваше потребление памяти увеличилось. Все имеет свою стоимость, вы не можете хранить что-то в памяти бесплатно. Следовательно, ваша программа ведет себя так, как и ожидалось.


Кстати, вам не нужно использовать лямбду, вы можете напрямую передать свою функцию:)

Edit: Для полноты вы должны прочитать @ Marek R's answer , в котором упоминается другая сторона topi c, которая является памятью, освобожденной программой (потоки, динамически выделяемые, ... ) не может быть немедленно возвращен в ОС.


Edit2:

Относительно вашего мнения об уменьшенном потреблении памяти при использовании мьютекса. Дело в том, что мьютекс заставляет все ваши потоки выполняться последовательно (по одному за раз).

Зная это, я полагаю, что компилятор может оптимизировать его, используя только один поток, и повторно использовать его 500 раз. Поскольку создание потока имеет свою стоимость (например, любой поток копирует стек), создание одного потока вместо 500 может значительно снизить потребление памяти.

2 голосов
/ 07 февраля 2020

Вся тайна скрыта в стандартной библиотеке, которая отвечает за управление памятью. Многопоточность влияет на потребление памяти только потому, что для каждого шага требуется достаточно много памяти (по некоторым причинам большинство новичков об этом не помнят).

Когда вы звоните delete (или free в C) ) это не значит, что память возвращается в систему. Это означает только то, что стандартная библиотека помечает этот фрагмент памяти как ненужный.

Теперь, поскольку запрос или освобождение памяти из / в систему довольно дорогой и может быть выполнен огромными кусками (размер страницы 8- 32 КБ в зависимости от аппаратного обеспечения), стандартная библиотека пытается оптимизировать это и не возвращает всю память обратно в систему немедленно. Предполагается, что программе может скоро понадобиться эта память.

Таким образом, память, используемая процессом, не является хорошим числом, указывающим на утечку памяти. Только когда процесс выполняется дольше, остается в том же состоянии и постоянно набирает память, вы можете заподозрить утечку памяти программой.
Во всех других случаях вам следует включать такие инструменты, как valgrind (я рекомендую использовать адресное дезинфицирующее средство) .

Есть и другая оптимизация, которая влияет на то, что вы видите. Порождение потока является дорогостоящим, поэтому, когда поток завершает свою работу, он не уничтожается полностью. Он хранится в «пуле потоков» для повторного использования в будущем.

1 голос
/ 07 февраля 2020

Я предполагаю, что у вас нет 500 ядер, поэтому некоторые потоки не будут работать одновременно, некоторые потоки завершат sh до последнего запуска, поэтому вы не можете использовать

S * sizeof (int) * n = 1000000 * 4 * 500 = 2000000000 (~ 2 ГБ)

получается, что вы максимально выделяете ~ 419 МБ, освобожденную память из первого затем повторно используются для последних потоков.

И программа не возвращает свою максимально использованную память ОС до завершения работы.

...