ithewei / libhv

🔥 比libevent/libuv/asio更易用的网络库。A c/c++ network library for developing TCP/UDP/SSL/HTTP/WebSocket/MQTT client/server.

Home Page:https://github.com/ithewei/libhv/wiki

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

TcpClient使用外部Loop注入方式构造,在高并发情况下,由于Loop线程池时异步排队执行,当手动释放TcpClient后,会出现崩溃情况?

LinHaoo opened this issue · comments

背景:业务需求,需要构造数百个TcpClient,每个TcpClient对应一个Loop方式不合适,所以参考了TcpClientEventLoop_test.cpp的例子,使用外部Loop注入方式构建TcpClient。

测试:因为机器只有4核,所以Loop默认为4个,首先有两个http接口,一个http接口时批量创建50个TcpClient,使用std::vector+智能指针保存TcpClient,然后start();另个http接口是调用vector.clear()全部释放TcpClient,循环调用这两个http接口,跑一段时间后,程序崩溃;

分析:gdb分析core文件,都是崩溃在connnect或者close阶段, 看了源码,好像Loop线程池是按照队列顺序执行任务,TcpClient是使用lamba+捕获this指针放到Loop里面去执行channel->connect、channel->close函数。当手动释放TcpClient后,该智能指针生命周期结束,释放资源,此时TcpClient变成野指针,但由于高并发,外部Loop此刻才轮到执行这个TcpClient的connect或者close,内部channel是使用this指针调用,当触发回调时,TcpClient已经是野指针了,所以程序崩溃?

分析的非常好,我觉得就是这个原因导致的.

TcpClient对象的生命周期应该至少在close回调后

TcpClient对象的生命周期应该至少在close回调后

作者你好,我也试过在close回调中设置删除标识,另个线程检测到删除标识+延时3秒后,才释放TcpClient智能指针,但仍然出现崩溃,崩溃地方指向TcpClient内部成员变量channel;
因为channel和this放到Loop线程池中执行,所以外部的TcpClient智能指针对其内部变量无法管理;而回调是由channel触发,但是没有哪个接口告诉channel什么时候已经没人用了,这时才应该释放TcpClient智能指针;
我也曾试过更改源码,有两个不成熟改动TcpClient的方案,1、TcpClient内部全部是智能指针变量,Loop线程池捕获智能指针,而不是捕获this指针;2、增加新接口的回调,在该回调中删除TcpClient;但由于对项目了解程度还不足对其改动,所以改不动。

根据libhv是借鉴muduo和evpp开发,所以我去muduo和evpp的GitHub都查阅了下,发现都找到了和这个issue类似的TcpClient崩溃的issue,尴尬的是最终都也没解决方案,其issue也没关闭,借用muduo库其他的回答,他总结的比较好在TcpClient析构时coredump
20240304154318

其实在提这个issue之前就已经提过类似的issue,但是当初也没解决;后来的解决方案是用夏楚大佬https://github.com/ZLMediaKit/ZLToolKit 网络库中TcpClient替换libhv的TcpClient,ZLTooklKit库特点就是就是全面用智能指针+weak_ptr,跑了大半年,都没出现TcpClient崩溃问题,后来发现可能是数据流要从libhv转发到zltookkit库,多了一次拷贝,造成性能上不去,所以才考虑换回libhv的TcpClient。

根据libhv是借鉴muduo和evpp开发,所以我去muduo和evpp的GitHub都查阅了下,发现都找到了和这个issue类似的TcpClient崩溃的issue,尴尬的是最终都也没解决方案,其issue也没关闭,借用muduo库其他的回答,他总结的比较好在TcpClient析构时coredump 20240304154318

channel就是用的智能指针,它是TcpClient里的一个成员变量,生命周期也是和TcpClient一致的,TcpClient析构时它随之析构,在close回调后不应该还有使用它的地方了。
可以试试先调用TcpClient::stop或者closesocket后,在close回调里,通过currentThreadEventLoop->runInLoop((){delete TcpClient;});投递一个事件在事件循环线程里去删除。

@LinHaoo 上面的commit添加了个deleteInLoop方法做线程安全的delete TcpClient对象,看看是否满足你的需要

@LinHaoo 上面的commit添加了个deleteInLoop方法做线程安全的delete TcpClient对象,看看是否满足你的需要

感谢作者,我后续再测试。昨天再次阅读了源码后,我发现close回调中,除了触发回调,后面还有个reconnect的操作(经分析如果在close回调中删除对象,接下来执行reconnect,相当于访问野指针),把这段代码屏蔽后,在close回调里面执行currentThreadEventLoop->runInLoop((){delete TcpClient;}); 目前运行了一天,暂无崩溃现象。
QQ截图20240306155547