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

问题描述 投票:0回答:2

我在具有 8GB 内存的 Fedora 35 系统上遇到与 C++ 内存管理相关的问题。具体来说,我正在使用两个线程,每个线程应该分配大约 4GB 内存,使用它,然后释放它。线程由

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++ multithreading heap-memory
2个回答
3
投票

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

您正在使用 glibc 的

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

您所看到的行为是这个特定的

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

因此,使用用于所有较小分配的通常的基于块的 arena 分配器。默认情况下,

malloc
将使用多个arena,直到达到某个上限,并尝试为不同的线程分配不同的arena,这样它们的分配就不会干扰。

此外,

malloc
实现会延迟在
free
之后释放竞技场使用的堆顶部的空闲内存,直到稍后的时间点,例如调用
malloc
,以避免释放然后立即重新获取内存,也许还可以保持
free
尽可能快。

因此,在线程中关闭

}
后,列表似乎已通过调用 glibc 的
free
完全释放,但
free
决定尚未将内存释放回系统。

因为另一个线程随后使用自己的堆在自己的竞技场中进行操作,所以它也不会将第一个线程的内存释放回操作系统,并且最终所需的内存量是预期的两倍。

您可以通过调用

malloc
,强制 }
 实现在 
malloc_trim(0)
之后将所有内存释放回操作系统。当然,这是不可移植的,只能在使用 glibc 或兼容替代方案的系统上运行。


我对glibc的

malloc
实现不太了解,所以上面的解释可能有错误的地方。有关实现的概述,请访问 https://sourceware.org/glibc/wiki/MallocInternals


-1
投票

答案在评论中给你,你深深地处于未定义的行为领域。但是,如果它有帮助的话,在 MSVC 中,程序的行为正如您所期望的:

当然,它会因

std::terminate
崩溃,因为您没有加入或分离第一个线程。

© www.soinside.com 2019 - 2024. All rights reserved.