多线程程序中的意外内存泄漏

Unexpected memory leak in multithread program

我正在开发一个使用大量线程的程序,每个线程在堆中分配几兆字节的内存。当这些线程结束时,程序会保留很大一部分RAM。

这是一个代码示例,在 500 个线程中分配和释放 1 MB,它显示了这个问题:

#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 构建,在消息 "Starting threads" 之前使用 1976 kB,在消息 "Threads ended" 之后使用 419 MB。这些值只是示例:当我多次 运行 程序时,我可以获得不同的值。

我试过 valgrind / memcheck,但没有检测到任何泄漏。

我注意到用互斥锁锁定 "std::fill" 操作似乎可以解决这个问题(或大大减少它),但我不认为这是一个竞争条件问题,因为没有共享记忆在这里。我想互斥体只是在线程之间创建了一个执行顺序,避免(或减少)内存泄漏的情况。

我正在使用 Ubuntu 18.04,GCC 7.4.0。

感谢您的帮助。

奥雷利安

根本没有内存泄漏,因为Valgrind/memcheck已经向您确认。

[...] uses 1976 kB before the message "Starting threads", and 419 MB after the message "Threads ended".

两件事:

  • 一开始,你的向量是空的。
  • 最后,您的矢量包含 500 std::future<void> 个对象。

这就是您的内存消耗增加的原因。一切都是有代价的,你不能在内存中免费存储一些东西。
因此,你的程序会按预期运行。


顺便说一下,你不需要使用 lambda,你可以直接传递你的函数:)

编辑: 为了完整起见,您应该阅读 ,其中提到了主题的另一面,即程序释放的内存(线程,动态分配,...) 可能不会立即返回到 OS.


编辑2:

关于您关于使用互斥体时减少内存消耗的观点。问题是互斥体强制所有线程按顺序执行(一次一个)。

知道了这一点,我猜编译器也许可以优化它,只使用一个线程并重复使用它500次。
由于创建线程是有成本的(例如,任何线程都会复制堆栈),因此创建一个线程而不是 500 个线程可以显着减少内存消耗。

我假设你没有 500 个核心,所以一些线程不会同时 运行,一些线程会在最后一个开始之前完成,这就是你不这样做的原因开始使用

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

发生的情况是您最多分配 ~419 MB,然后从第一个线程释放的内存将重新用于最后一个线程。

并且程序在退出前不会return其最大使用内存到OS。

整个谜团都隐藏在负责管理内存的标准库中。多线程对内存消耗有影响只是因为每个线程都需要相当多的内存(出于某些原因大多数初学者不记得这一点)。

当您调用 delete(或 C 语言中的 free)时,并不意味着内存 return 到系统。这只是意味着标准库将这块内存标记为不再需要。

现在由于请求或释放内存 from/to 系统非常昂贵并且可以大块完成(页面大小为 8-32 kB 取决于硬件),标准库尝试优化它但没有return 立即将所有内存还给系统。它假定程序可能很快再次需要此内存。

所以进程消耗的内存不是指示内存泄漏的好数字。只有当进程运行的时间比较长,一直处于同一个状态,不断获取内存时,才可以怀疑是程序内存泄露。
在所有其他情况下,您应该依赖 valgrind 等工具(我建议使用地址消毒器)。

还有其他优化会影响您所看到的内容。生成线程的成本很高,因此当线程完成其工作时,它不会被完全销毁。它保存在 "thread pool" 中以供将来重复使用。