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

场景

使用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;
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在 C++ 中,shared_ptr 常常被用来管理动态分配的资源。然而,当多个 shared_ptr 相互引用时,就会出现循环引用的问题,导致内存泄漏。为了解决这个问题,C++11 引入了 weak_ptr。 weak_ptrshared_ptr 的一种扩展,它可以指向一个由 shared_ptr 管理的对象,但并不拥有该对象的所有权。weak_ptr 可以被用来解决 shared_ptr 循环引用的问题。 当一个对象被多个 shared_ptr 共享时,每一个 shared_ptr 都会增加该对象的引用计数。如果其中一个 shared_ptr 被销毁时,该对象的引用计数会减少。但如果多个 shared_ptr 相互引用,就会导致循环引用的问题。例如: ```c++ class B; class A { public: std::shared_ptr<B> b_ptr; }; class B { public: std::shared_ptr<A> a_ptr; }; int main() { std::shared_ptr<A> a(new A); std::shared_ptr<B> b(new B); a->b_ptr = b; b->a_ptr = a; return 0; } ``` 在上面的代码中,A 和 B 互相引用,它们的引用计数永远不会为 0,导致内存泄漏。为了解决这个问题,我们可以将其中一个 shared_ptr 改为 weak_ptr。例如: ```c++ class B; class A { public: std::weak_ptr<B> b_ptr; }; class B { public: std::shared_ptr<A> a_ptr; }; int main() { std::shared_ptr<A> a(new A); std::shared_ptr<B> b(new B); a->b_ptr = b; b->a_ptr = a; return 0; } ``` 在这个例子中,A 持有一个指向 B 的 weak_ptr,而 B 持有一个指向 A 的 shared_ptr。这样,当 A 或 B 中的任意一个 shared_ptr 被销毁时,它们所指向的对象的引用计数都会减少,从而解决了循环引用的问题。 需要注意的是,当通过 weak_ptr 访问对象时,需要先将 weak_ptr 转换为 shared_ptr,否则无法访问对象。假设上面的例子中,我们需要访问 B 对象,可以这样做: ```c++ std::shared_ptr<B> b_ptr = a->b_ptr.lock(); if (b_ptr) { // 访问 B 对象的成员 } ``` 在上面的代码中,我们使用 lock() 方法将 weak_ptr 转换为 shared_ptr,如果转换成功,就可以访问 B 对象的成员了。 总之,weak_ptrshared_ptr 的一种扩展,可以用来解决 shared_ptr 循环引用的问题。通过将其中一个 shared_ptr 改为 weak_ptr,可以防止循环引用导致的内存泄漏
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值