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