360的evpp库的一处内存泄漏分析,shared_ptr的循环引用

本文分析了一个使用EvPP和EvNSQ库的C++程序中存在的内存泄漏问题。该程序通过HTTP查询nsqlookupd并连接到nsq,但因未能正确释放请求对象导致内存泄漏。文章详细解释了内存泄漏的原因,并提供了一个简单的示例来说明问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

场景

使用evpp的evnsq的代码连接nsqlookupd进行Http查询,然后连接nsq,存在内存泄漏。

类似于这样的代码:

#include <evnsq/exp.h>
#include <evnsq/consumer.h>
#include <evpp/event_loop.h>


int OnMessage(const evnsq::Message* msg) {
    LOG_INFO << "Received a message, id=" << msg->id << " message=[" << msg->body.ToString() << "]";
    return 0;
}

int main(int argc, char* argv[]) {
    std::string nsqd_tcp_addr;
    std::string lookupd_http_url;
    nsqd_tcp_addr = "127.0.0.1:4150";
    lookupd_http_url = "http://127.0.0.1:4161/lookup?topic=test";

    if (argc == 2) {
        if (strncmp(argv[1], "http", 4) == 0) {
            lookupd_http_url = argv[1];
        } else {
            nsqd_tcp_addr = argv[1];
        }
    }

    evpp::EventLoop loop;
    evnsq::Consumer client(&loop, "test", "ch1", evnsq::Option());
    client.SetMessageCallback(&OnMessage);

    if (!lookupd_http_url.empty()) {
        client.ConnectToLoopupds(lookupd_http_url);
    } else {
        client.ConnectToNSQDs(nsqd_tcp_addr);
    }

    loop.Run();
    return 0;
}

代码分析

先看client.cc的ConnectToLookupd函数,这边申请了一份内存Request,后面我们分析Request没有被释放。

void Client::ConnectToLookupd(const std::string& lookupd_url/*http://127.0.0.1:4161/lookup?topic=test*/) {
    auto f = [this, lookupd_url]() {
        std::shared_ptr<evpp::httpc::Request> r(new evpp::httpc::Request(this->loop_, lookupd_url, "", evpp::Duration(1.0)));
        LOG_ERROR << "query nsqlookupd " << lookupd_url << ";request=" << r << ";real=" << r.get() << ";use_count=" << r.use_count();
        r->Execute(std::bind(&Client::HandleLoopkupdHTTPResponse, this, std::placeholders::_1, r));
		LOG_ERROR << "query nsqlookupd end,use_count=" << r.use_count();
    };

    // query nsqlookupd immediately right now
    loop_->RunInLoop(f);

    // query nsqlookupd periodic
    auto timer = loop_->RunEvery(option_.query_nsqlookupd_interval, f);//ƴ????ʱȥ?鑯nsq-lookup?ĸ????شimer,ҲΪ?󃦿Ʉܒ?cancle?􍊠   lookupd_timers_.push_back(timer);
}

这边加了打印,r的引用计数在执行完bind之后增加了变成了2,说明了std::bind持有了r的引用。

 

再看r->Execute的实现,h赋值给了handler_,所以变成了r自身的handler_持有了自身的引用。吐槽一下,真是不好的实现。

void Request::Execute(const Handler& h) {
	handler_ = h;
    loop_->RunInLoop(std::bind(&Request::ExecuteInLoop, this));
}

所以在Client::HandleLoopkupdHTTPResponse函数中request的引用计数是1,如果request的handler_没有被置空,那么request永远不会被释放,造成了内存泄漏。

void Client::HandleLoopkupdHTTPResponse(
    const std::shared_ptr<evpp::httpc::Response>& response,
    const std::shared_ptr<evpp::httpc::Request>& request) {
    DLOG_TRACE;
	
	LOG_ERROR << "HandleLoopkupdHTTPResponse request=" << request.get() << ";use_count=" << request.use_count();
    if (response.get() == nullptr) {
        LOG_ERROR << "Request lookupd http://" << request->conn()->host() << ":"
            << request->conn()->port() << request->uri()
            << " failed, response is null";

        return;
    }

    std::string body = response->body().ToString();
    if (response->http_code() != 200) {
        LOG_ERROR << "Request lookupd http://" << request->conn()->host() << ":"
                  << request->conn()->port() << request->uri()
                  << " failed, http-code=" << response->http_code()
                  << " [" << body << "]";
        return;
    }

    rapidjson::Document doc;
    doc.Parse(body.c_str());

    rapidjson::Value& producers = doc["producers"];
    for (rapidjson::SizeType i = 0; i < producers.Size(); ++i) {
        rapidjson::Value& producer = producers[i];
        std::string broadcast_address = producer["broadcast_address"].GetString();
        int tcp_port = producer["tcp_port"].GetInt();
        std::string addr = broadcast_address + ":" + std::to_string(tcp_port);

        if (!IsKnownNSQDAddress(addr)) {
            ConnectToNSQD(addr);
        }
    }
}

写一个类似的例子:

#include <iostream>
#include <functional>
#include <stdlib.h>
#include <memory>
#include <list>
#include <unistd.h>

using namespace std;


using msgCallback = std::function<void()>;
 
class A {
public:
    ~A() { 
        cout << "A::~A()" << endl; 
    }
    void output() {
        cout << "A::output()" << endl;
    }
};
 
class B {
public:
    ~B() {
        cout << "B::~B()" << endl;
    }
    void setCallback(const msgCallback& cb) {
        if (m_cb == nullptr)
            m_cb = cb;
    }
    void resetCallback() {
        m_cb = nullptr;
    }
private:
    msgCallback m_cb;
};



int main()
{
    shared_ptr<B> spb = make_shared<B>();
    {
        shared_ptr<A> spa = make_shared<A>();   //use_count:1
 
        list<shared_ptr<A>> lst;
        lst.push_back(spa);     //use_count:2
        cout << "count1: " << spa.use_count() << endl;
        {
            std::function<void()> f = std::bind(&A::output, spa);   //use_count:3
            cout << "count1.5: " << spa.use_count() << endl;
            spb->setCallback(f);    //use_count:4
            cout << "count2: " << spa.use_count() << endl;
        }   //use_count:3
        cout << "count3: " << spa.use_count() << endl;
 
        if (!lst.empty())
            lst.erase(lst.begin());     //use_count:2
        cout << "count4: " << spa.use_count() << endl;
    }   //use_count:1
 
    //期望spa在lst.erase后就析构
    //结果spb->m_cb还具有spa的一份引用,导致A不能按预期析构
 
    while(1)
    {
      sleep(5);
      cout << "sleep 5" << endl;
      break;
    }
    return 0;
}

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值