You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Fedora 35下C++多线程大列表内存释放异常问题咨询

问题:C++多线程内存占用异常增长

在8GB内存的Fedora 35系统中运行C++程序时遇到内存管理问题:两个线程通过std::mutex互斥,确保同一时间只有一个线程进行内存分配/释放,每个线程需分配约4GB内存,使用后释放。

预期程序总内存占用不会超过4GB(含程序及线程开销),但实际内存占用持续增长,直到系统耗尽内存终止进程。已知C++释放的内存可能不会立即归还操作系统,会留存供程序后续复用,但此处第一个线程释放的内存未被第二个线程复用,导致内存占用过高。

疑问:为何内存未按预期释放?如何确保内存不再需要后立即释放?

相关代码:

#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;
}
解答

一、内存未复用的核心原因

  1. 线程本地内存缓存的隔离:
    现代堆分配器(比如Fedora默认的glibc ptmalloc2)会为每个线程创建独立的线程本地缓存(TLC)。第一个线程分配的4GB内存会存入自身的TLC,释放时内存只会回到该线程的TLC中,而非全局堆或操作系统。第二个线程作为独立线程,无法访问其他线程的TLC,只能从全局堆或操作系统申请新内存,导致总内存占用叠加。
  2. 线程存活导致缓存无法回收:
    第一个线程在释放内存后进入死循环,线程始终存活,其TLC会一直保留占用的内存,既无法被其他线程复用,也不会归还给操作系统。

二、解决办法

1. 让线程自然退出,释放本地缓存

移除第一个线程的死循环,让线程在完成内存释放后自然退出。线程退出时,其TLC会被销毁,内存会归还给全局堆,后续其他线程可以复用这些内存。

修改后的manageList1:

void manageList1() {
    mtx.lock();
    {
        std::list<std::string> myList1;

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

        myList1.clear();
    }
    mtx.unlock();
    // 移除死循环,线程自动退出
}

同时修改main函数,等待两个线程完成:

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

    listThread1.join();
    listThread2.join();

    return 0;
}

2. 强制归还内存给操作系统

如果必须保持第一个线程存活,可以调用堆分配器的接口,强制将线程本地缓存中未使用的内存归还给操作系统。对于glibc,使用malloc_trim(0)函数:

#include <malloc.h> // 需包含此头文件

void manageList1() {
    mtx.lock();
    {
        std::list<std::string> myList1;

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

        myList1.clear();
    }
    mtx.unlock();
    malloc_trim(0); // 强制将未使用内存归还操作系统
    while(true){}
}

注意:malloc_trim是glibc特有的接口,其他平台需使用对应替代(如Windows的HeapCompact)。

3. 禁用线程本地缓存

通过设置环境变量调整glibc的堆分配器行为,禁用线程本地缓存,让所有线程共享全局内存池:

export MALLOC_ARENA_MAX=1

运行程序时先执行上述命令,这样释放的内存会回到全局堆,可被其他线程复用。但此方法会降低多线程内存分配的性能,因为所有线程的分配操作需要竞争全局锁。

4. 使用自定义分配器

为std::list和std::string实现不依赖线程本地缓存的自定义分配器,让内存直接从全局堆分配和释放。此方法适合对内存控制有严格要求的场景,但实现复杂度较高。


内容的提问来源于Stack Exchange,提问作者Mehdi At

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 12:57:37