将析构调用转移至专用线程:C++代码实现有效性问询
我在优化C++代码时遇到析构调用耗时过长的问题,为此编写了Cleaner类尝试将析构操作转移至其他线程,初步测试可行。现在想确认:
- 该代码是否能如预期般将
shared_ptr中的数据所有权从调用线程转移至专用线程,从而将内存释放开销转移至该线程? - 是否存在重大隐患或副作用?
代码实现
#pragma once #include <queue> #include <mutex> #include <memory> #include <functional> namespace test { class Cleaner { struct State { std::queue<std::shared_ptr<void>> queue; std::mutex mtx; std::condition_variable condition; bool cancelled; bool done; }; public: Cleaner() : _state(std::make_shared<State>()), _thread(&runCleanerloop, _state) { } ~Cleaner() { { std::lock_guard<std::mutex> lock(_state->mtx); _state->done = true; } _state->condition.notify_all(); _thread.join(); } template <typename T> void run(std::shared_ptr<T>& data) { { std::lock_guard<std::mutex> lock(_state->mtx); // Push a copy to the queue to be released by the thread. _state->queue.push(data); // Reset the input in the caller's thread. data.reset(); } _state->condition.notify_all(); } static void runCleanerloop( const std::shared_ptr<State>& state ) { std::shared_ptr<void> data; bool done = false; while (true) { // Wait for data to appear in queue. { std::unique_lock<std::mutex> lock(state->mtx); state->condition.wait(lock, [&state] { return state->cancelled || state->queue.size() || state->done; }); if (state->cancelled || (!state->queue.size() && state->done)) { done = true; } else { data = state->queue.front(); state->queue.pop(); } } state->condition.notify_all(); if (done) { return; } // Release the data inside this thread. data.reset(); } } private: std::shared_ptr<State> _state; std::thread _thread; }; }
预期用法
#include "cleaner.h" void test() { Cleaner cleaner; std::shared_ptr<Data> current_data; while (true) { current_data = createData(); /* Do something */ // This will reset the current_data ptr, but the actual destructor-call // will be done inside the Cleaner thread. cleaner.run(current_data); } }
补充说明:当然,相比此类实现,更优方案是设计合理的内存管理系统,避免循环中重复分配内存;若存在其他shared_ptr持有数据,则由于引用计数未归零,专用线程不会销毁数据。
回答
一、是否达到预期效果?
是的,这个实现确实能将析构开销转移到Cleaner线程:
- 调用
cleaner.run(current_data)时,会先把shared_ptr的副本推入队列,此时引用计数+1;随后在调用线程reset原指针,引用计数回到1(仅队列中的副本持有)。 - 当Cleaner线程取出队列中的
shared_ptr并reset时,引用计数归零,析构函数和内存释放会在Cleaner线程执行,完全避开了调用线程的开销。 - 你补充说明的「若存在其他
shared_ptr持有数据,则专用线程不会销毁数据」也是正确的,因为此时引用计数不会归零,析构会等到最后一个shared_ptr被销毁时才触发。
二、存在的隐患与副作用
- 队列堆积与内存占用风险:如果调用线程产生待销毁对象的速度远快于Cleaner线程的处理速度,队列会持续堆积,导致内存占用飙升,甚至触发OOM。尤其是在循环创建销毁的场景中,这个问题会被放大。
- 不必要的条件变量通知:
run函数每次push后调用notify_all(),实际上notify_one()就足够唤醒等待的Cleaner线程,notify_all()会带来额外的线程唤醒开销。- Cleaner线程在每次处理完一个对象后调用的
notify_all()完全多余,没有其他线程等待该操作完成,只会徒增开销。
- 无效的
cancelled状态:代码中定义了cancelled标志,但没有提供外部触发的接口,属于无效代码,容易造成维护混淆。 - 线程安全边界问题:虽然Cleaner的析构逻辑是安全的,但如果多线程同时调用
run并同时触发Cleaner析构,需要确保Cleaner对象的生命周期管理正确,避免析构时还有线程调用run。 - 性能权衡问题:每个
run调用都需要加锁、操作队列,这些操作本身有一定开销。如果单个对象的析构开销很小,转移到其他线程的收益可能抵不上锁和队列操作的开销,反而降低整体性能。
三、优化建议
- 将
notify_all()替换为notify_one(),减少不必要的线程唤醒。 - 移除无用的
cancelled状态,或者补充对应的取消接口(比如cancel()方法)。 - 考虑给队列设置最大容量,当队列满时让调用线程阻塞或根据业务场景选择丢弃策略,避免内存无限增长。
- 可以批量处理队列中的对象,减少锁的竞争次数(比如一次取出多个对象再统一销毁)。
- 优先从内存管理架构上优化,比如使用对象池复用对象,从根源上减少频繁创建销毁的开销,这比转移析构开销的收益更大。
内容的提问来源于stack exchange,提问作者4-bit
相关产品推荐
相关产品推荐

