【问题标题】:Unexpected memory leak in multithread program多线程程序中的意外内存泄漏
【发布时间】:2020-05-23 08:57:15
【问题描述】:

我正在开发一个使用大量线程的程序,每个线程在堆中分配几兆字节的内存。当这些线程结束时,程序会保留大部分 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 构建的,在消息“开始线程”之前使用了 1976 kB,在消息“线程结束”之后使用了 419 MB。这些值只是示例:当我多次运行程序时,我可以得到不同的值。

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

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

我正在使用带有 GCC 7.4.0 的 Ubuntu 18.04。

感谢您的帮助。

奥雷利安

【问题讨论】:

    标签: c++ multithreading memory-leaks


    【解决方案1】:

    正如 Valgrind/memcheck 已经向您确认的那样,根本没有内存泄漏。

    [...] 在消息“开始线程”之前使用 1976 kB,在消息“线程结束”之后使用 419 MB。

    两件事:

    • 一开始,您的向量是空的。
    • 最后,您的向量包含 500 std::future&lt;void&gt; 对象。

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


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

    编辑:为了完整起见,您应该阅读@Marek R's answer,其中提到了该主题的另一面,即程序释放的内存(线程,动​​态分配,...)可能不会立即返回给操作系统。


    编辑2:

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

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

    【讨论】:

    • 感谢您的回答,但我试图清除或完全删除std::future&lt;void&gt; 的向量,它不会改变结果。此外,它没有解释为什么当我添加互斥体时问题消失了(在这种情况下,我的向量中仍然有 500 个std::future&lt;void&gt;)。
    • @Aurélien 这是正常的,因为清除向量不会释放分配的空间。如果清除向量,则有效大小变为零,但在后面,向量保持可用空间(其内部分配的缓冲区)。 See this 清除后向量容量仍为512。这就是std::vector 的工作原理。
    • 是的,我试过这个,看看是否调用析构函数会有所帮助。但是,当我使用互斥锁锁定任务时,使用的内存要少得多这一事实呢?
    • @Aurélien 我也编辑了关于互斥锁的答案。另一件事,不要为留在堆栈上的对象调用析构函数,因为它会被销毁两次(并因此导致未定义的行为)。相反,您可以在清除后使用std::vector::shrink_to_fit() 以删除未使用的容量。
    【解决方案2】:

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

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

    现在,由于向系统请求或释放内存非常昂贵,并且可以大块完成(页面大小为 8-32 kB,具体取决于硬件),标准库会尝试对其进行优化并且不会返回所有内存立即到系统。它假定程序可能很快会再次需要此内存。

    因此进程消耗的内存不是一个很好的数字,表明内存泄漏。只有当进程运行较长时间,保持相同状态并不断获得内存时,才可以怀疑程序泄漏内存。
    在所有其他情况下,您应该使用 valgrind 等工具(我建议使用地址清理器)。

    还有其他优化会影响您所看到的内容。生成线程的成本很高,因此当线程完成其工作时,它并没有完全销毁。它保存在“线程池”中以备将来重用。

    【讨论】:

    • 新建/删除通常调用 malloc/free(至少对于 libstd++ 和 libc++)。 malloc 基于 brk/sbrk/mmap stackoverflow.com/questions/5716100/…。 AFAIK 释放的内存只有在brk 的末尾才能返回到系统。否则,它将可用于后续调用malloc
    【解决方案3】:

    我假设你没有 500 个内核,所以一些线程不会同时运行,一些线程会在最后一次启动之前完成,这就是你不能使用的原因

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

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

    并且程序在退出之前不会将其最大使用内存返回给操作系统。

    【讨论】:

      猜你喜欢
      • 2015-08-14
      • 2021-11-26
      • 2014-10-29
      • 2013-12-18
      • 1970-01-01
      • 1970-01-01
      • 2012-06-14
      • 2017-02-04
      • 2016-12-09
      相关资源
      最近更新 更多