C++线程回收误用,导致批量设备随机掉线!
[TOC]
脚步不停,终达卓越!更多优质文章及代码资源详见公众号 《开源519》
引言
前段时间,项目上出现一个严重的设备掉线问题,导致客户投诉,简单记录一下前因后果。
场景
问题描述:
[高优先级] xxx版本 - 远程批量控制终端时触发设备随机异常掉线,且掉线后无法自动恢复。-
复现步骤:
- 后台对运行xxx版本的终端发起远程批量控制操作。
- 控制下发后,受控设备存在随机异常掉线,且无法自动恢复,手动重启正常(高频率出现)。
根因排查
困难所在
每次遇到这种概率性问题,解决起来都比较困难,主要原因有下:
- 本地无法复现: 本地测试环境(网络条件、负载压力、周边设备干扰)与实际部署的复杂环境无法对齐,导致无法稳定复现问题。
- 现场日志获取困难: 设备已交付分散各地,实地状态确认和关键日志提取(需现场操作/设备可能无响应)耗时耗力且困难。
- 无法找到触发规律: 从描述看,问题触发具有较高随机性,实际中频繁出现,但本地测试难以复现,无法找到明确触发规律。
- 根因定位复杂: 掉线问题可能由多种深层问题(资源竞争、泄漏、协议异常等)引起,需要分析海量信息寻找线索。
分析过程
1. 测试同事,好不容易协调好设备上线,远程取了日志后,立马呈给开发大佬排查。
2. 像这种问题,首先过滤socket
状态日志。从日志上看,似乎是远程服务器断开了客户端的连接:
recv error: Connection reset by peer
3. 于是,压力给到服务器侧研发。服务器侧同事拿到问题后,给出结论:是因为存在相同的终端信息登入,导致设备被踢。
4. 问题甩到了终端侧研发,看到此结论,首先怀疑是否有不同的终端误刷了相同的设备ID。
5. 测试同事坚决否认:货设备ID管控,不可能误刷相同信息。
6. 终端研发无奈只能结合日志,逐行排查控制业务相关的代码。
7. 星光不负赶路人,经过苦思冥想和 "地中海" 加成,终于在代码实现上,找到一些蛛丝马迹。
代码定位
问题代码大致如下:
void DeviceConnection::reconnect() {
...
if (m_worker.joinable()) {
... // 省略 触发socket关闭
m_worker.detach();
}
m_worker = std::thread([this] {
... // 省略 socket连接
});
}
最终定位到此处,从代码上分析:
- 本意应该是先关闭
socket
,然后回收线程。 - 问题就在于
detach
是非阻塞的,并不会等待socket
完全关闭。这就导致了在新线程创建新的socket
连接时,上一个socket
可能还没关闭。 - 这也就与实际现象吻合看,如果
socket
关闭的够快,就不会存在这种问题。而本地复现时,CPU
处理的业务往往没有实际场景复杂,故难以复现。
解决方案
找到问题了,修复起来也就快了。主要有两种方式:
- 将
detach()
替换成join()
,可快速解决问题。 - 将重连逻辑优化至一个线程处理,避免重复创建线程,但改动较大,实施复杂。
鉴于当前需快速、稳妥地修复问题,选择了第一种方案。虽然从长远看第二种方案更优,但受限于项目进度,第一种方案是当下最优解。后续类似功能的开发,可参考第二种方案。
总结
回头再看这个紧急问题,根源在于误用了线程detach()
。后续开发中,回收线程尽量不要使用detach()
。