Fedora 35 上带有线程的大型列表中的 C++ 内存释放

C++ Memory De-allocation in Large Lists with Threads on Fedora 35

提问人:Mehdi At 提问时间:6/17/2023 最后编辑:genpfaultMehdi At 更新时间:10/25/2023 访问量:120

问:

我在具有 8GB 内存的 Fedora 35 系统上遇到与 C++ 内存管理相关的问题。具体来说,我正在处理两个线程,其中每个线程应该分配大约 4GB 的内存,使用它,然后解除分配它。线程使用 a 进行控制,以确保一次只有一个线程分配或解除分配内存。std::mutex

这是我正在使用的代码:

#include <thread>
#include <list>
#include <string>
#include <mutex>

std::mutex mtx;

// Function for first thread
void manageList1() {
    mtx.lock();
    {
        std::list<std::string> myList1;

        for(int i = 0; i < 50000; ++i) {
            myList1.push_back(std::string(80000, 'a')); // Approx. 4GB
        }

        myList1.clear(); // Clear list
    }
    mtx.unlock();

    while(true){} // Keep thread 1 from exiting
}

// Function for second thread
void manageList2() {
    mtx.lock();
    {
        std::list<std::string> myList2;
        for(int i = 0; i < 50000; ++i) {
            myList2.push_back(std::string(80000, 'a')); // Approx. 4GB
        }

        myList2.clear(); // Clear list
    }
    mtx.unlock();
}

int main() {
    std::thread listThread1(manageList1);
    std::thread listThread2(manageList2);

    // Don't join listThread1
    listThread2.join();

    return 0;
}

我预计该程序的总内存使用量不会超过大约 4GB(加上程序和线程的开销),因为一个线程应该在另一个线程开始分配之前释放其内存。但是,我观察到的是内存使用量逐渐增加,直到系统耗尽内存并终止进程。

我知道在 C++ 中,释放的内存不一定会立即返回到操作系统,并且可能会保留它以供同一程序将来分配。但是,在这种情况下,第二个线程似乎没有重用第一个线程的内存,这会导致内存使用过多。

我将不胜感激对这个问题的任何见解。为什么内存未按预期释放?有没有办法确保内存在不再需要后立即释放?

C++ 多线程 堆内存

评论

2赞 Solomon Slow 6/17/2023
显然,这是某种实验,但它的目的是证明什么?你有三个线程(包括主线程),但你绝不允许其中任何两个同时做任何有趣的事情。这有什么意义呢?
3赞 user17732522 6/17/2023
while(true){}导致未定义的行为。像这样的无限循环是不允许的。此外,您永远不会加入线程,如果它不是 UB,则会导致程序退出并调用 。std::terminate
2赞 user4581301 6/17/2023
旁注:在某种程度上,你很幸运。由于程序没有描述任何可观察的行为,因此它不做任何事情,编译器可以优化整个过程,从而有效地生成.int main(){}
4赞 PaulMcKenzie 6/17/2023
无论如何,该程序从根本上是有缺陷的。 如果抛出异常,则有一个永久锁定的互斥锁。请改用。mtx.lock();push_backstd::lock_guard
1赞 user17732522 6/18/2023
@HolyBlackCat 解释可能是 sourceware.org/glibc/wiki/MallocInternals 的某个地方,结合了程序执行的分配和解除分配的顺序。

答:

-1赞 Blindy 6/17/2023 #1

答案在评论中给你,你深深地陷入了不确定的行为领域。但是,如果它有帮助,在 MSVC 中,程序的行为符合您的预期:

enter image description here

然后当然,它会崩溃,因为你没有加入或分离第一个线程。std::terminate

评论

0赞 HolyBlackCat 6/17/2023
你是说缺乏是导致无限内存使用的原因吗?我不买。我会告诉 OP 再次尝试这些更改以确认。while(true) {}.join()
0赞 Blindy 6/17/2023
我是说 MSVC 中没有无限的内存使用量。
0赞 HolyBlackCat 6/17/2023
那么问题是特定于编译器的吗?我认为这应该是一个评论。
0赞 Blindy 6/17/2023
另外需要明确的是,如果没有看到显示这种无限内存分配的内存图,我就不相信 OP 的表面价值。即使我这样做了,如果没有进一步的证据,我也不会相信结果与他的代码有关。以上是具体的证据,证明他的描述在带有 MSVC 的 Windows 中是不正确的。
0赞 user17732522 6/17/2023
@HolyBlackCat 如果他们使用 Clang,可能只是无限循环是问题所在。在带有 Clang 的 godbolt 上,它会以 、 中止并带有无效的诊断或只是一个安静的 segault,具体取决于使用的确切数字。-O2std::terminatefree()
3赞 user17732522 6/18/2023 #2

根据评论中的讨论,这是我最好的猜测:

您正在使用 glibc 的实现,这是字符串的内存分配请求最终结束的地方。(glibc 是 Linux 发行版上最常见的 C 标准库实现提供者,但还有其他像 musl 这样的提供者)malloc

您看到的行为是实现此特定方式的副作用。特别是,它不认为字节足够大,无法完全单独使用 为每个字符串分配和释放内存。默认限制为 128k,可以在代码中设置(使用不可移植的 mallopt)以及环境变量。malloc80000mmapMALLOC_MMAP_THRESHOLD_

因此,使用通常用于所有较小分配的基于块的竞技场分配器。默认情况下,将使用多个竞技场,直到某个上限,并尝试为不同的线程分配不同的竞技场,以便它们的分配不会干扰。malloc

此外,该实现会延迟释放竞技场使用的堆顶部的可用内存,直到稍后的时间点,例如调用 ,以避免释放然后立即重新获取内存,并且可能还保持尽可能快的速度。mallocfreemallocfree

因此,似乎在线程关闭后,列表已通过调用 glibc's 完全释放,但决定尚未将内存释放回系统。}freefree

由于另一个线程随后使用自己的堆在自己的领域中运行,因此它也不会将第一个线程的内存释放回操作系统,并且您最终需要的内存量是预期的两倍。

您可以强制实现在调用 malloc_trim(0) 后将所有内存释放回操作系统。当然,这是不可移植的,并且仅适用于使用 glibc 或兼容替代方案的系统。malloc}


我对glibc的实现了解不多,所以上面的解释可能有错误的地方。有关实施的概述,请参阅 https://sourceware.org/glibc/wiki/MallocInternalsmalloc