单生产者单消费者WorkerQueue内存未释放问题求助
嘿,我帮你拆解下这个WorkerQueue的内存释放问题——你遇到的差异其实是Linux内存分配器的特性加上用std::vector做队列的低效设计共同导致的,虽然Packet的析构函数确实被调用了1000次,但内存没有立即归还给操作系统。咱们一步步解决:
问题根源
1. std::vector做队列是个糟糕的选择
你用std::vector存储队列元素,每次处理时调用queue_.erase(begin(queue_))——这个操作会把vector里剩下的所有元素往前移动一位,时间复杂度是O(n),而且vector的容量(capacity)不会自动缩小。哪怕所有元素都被析构了,vector还是会保留它曾经达到的最大容量对应的缓冲区(虽然这个缓冲区本身占不了多少内存,但它会影响内存分配器的行为)。
2. Linux内存分配器不会主动归还内存
Linux下的glibc malloc(ptmalloc)对于大块内存(比如你每个Packet里的1MB数据),如果是用mmap分配的,释放时会立刻还给系统;但如果是从进程的堆池里分配的,释放后会被堆管理器留着,用来应付后续的内存请求,不会立刻还给操作系统。这就是两种场景差异的原因:
- 先填充再Start:一次性分配1000个1MB块,malloc大概率直接用
mmap分配,释放时直接munmap还给系统,所以内存占用会慢慢降下来。 - 先Start再填充:逐个分配1000个1MB块,堆管理器可能把这些内存合并到进程堆里,释放后不会立刻还给系统,所以你看top里的RES内存还是1GB,但实际上进程的可用堆内存已经被释放了。
解决方案
1. 把std::vector换成std::queue(强烈推荐)
std::queue默认用std::deque当底层容器,pop()操作是O(1)的,不会有元素移动的开销,内存管理也更高效。修改后的WorkerQueue代码如下:
template<class T> struct WorkerQueue { void Start() { t_ = std::move(thread{&WorkerQueue::Run, this}); } void Enqueue(T t) { std::lock_guard<std::mutex> l{m_}; queue_.push(std::move(t)); cv_.notify_one(); } void Run() { while(true) { T item; { std::unique_lock<std::mutex> l{m_}; cv_.wait(l, [&](){return !queue_.empty();}); // 移动队首元素出来,pop时会清空队列中的位置 item = std::move(queue_.front()); queue_.pop(); std::cout << "q_size: " << queue_.size() << endl; } // 模拟工作负载 this_thread::sleep_for(50ms); } } std::queue<T> queue_; // 替换成std::queue thread t_; mutex m_; condition_variable cv_; };
2. 强制内存归还给系统(可选,仅Linux)
如果你希望内存立刻还给操作系统,可以在队列空的时候调用glibc的扩展函数malloc_trim(0),它会强制堆管理器把空闲内存还给系统。记得要包含malloc.h头文件:
#include <malloc.h> // 在Run函数里,当队列空时触发 void Run() { while(true) { { std::unique_lock<std::mutex> l{m_}; cv_.wait(l, [&](){return !queue_.empty();}); queue_.erase(begin(queue_)); std::cout << "q_size: " << queue_.size() << endl; // 队列空时,收缩vector并强制释放内存 if (queue_.empty()) { queue_.clear(); queue_.shrink_to_fit(); malloc_trim(0); } } this_thread::sleep_for(50ms); } }
3. 简化Packet的析构函数
你Packet析构里的data_ = std::vector<char>();完全多余,vector的析构函数会自动释放所有内存,删掉这行代码不影响功能,还能减少不必要的操作:
struct Packet { explicit Packet(size_t size) { data_.resize(size); } virtual ~Packet() { std::cout << "~Packet();\n"; // 去掉多余的data_清空操作 } std::vector<char> data_; };
验证效果
修改后,先调用wq.Start()再填充元素,等所有Packet处理完后,内存占用会逐步下降(用malloc_trim的话会立刻下降),而且WorkerQueue的处理速度也会快很多(不用再移动vector里的元素了)。
内容的提问来源于stack exchange,提问作者user4573087

