记录使用map遇到的很奇怪的问题,map find()陷入死循环

1.背景

最近在一个项目中使用const char*作为map的key,然后定制比较器,一般情况下程序运行没问题,但是在线上运行时会有一定几率发生“死锁”的情况,直观表现就是程序一直不退出,内存占用很稳定,不增不减,

CPU在占用,并且有波动,但根据占用率数据来看是一个线程在跑,如果死循环的话不应该是没有CPU占用了吗?

好吧,进行了两天多的调试分析,修改,终于定位出问题所在了。是map.find()时候发生了死循环,导出当前锁的占用者线程卡在find()里不出来,也就无法释放锁;而其他线程在等待锁,一直拿不到锁,所以直观表现就是程序一直运行,CPU占用来看是一个线程一直在跑(陷入死循环的线程)。

2.分析过程

2.1.准备

2.2.生成DMP文件

因为线上有一定概率会重现该问题,当重现该问题时需要打开任务管理器,选中该进程,右键选择创建转储文件,然后就在临时目录生成DMP文件了;

当然也可以在线上环境用Windbg附加到目标进程,用命令的方式创建DMP文件,参见windbg使用教程(调试异常及死锁等),用windbg附加的另一个好处是可以实时调试进程,比如使用Windbg的go、step out、step into、step over、break等命令调试进程,

实时的查看相关数据(比如~*kb查看所有线程的堆栈信息)。

2.3.调试分析DMP文件

  • 设置调试前相关配置

拿到DMP文件后,用VS打开,设置符号目录,

设置了符号路径后,点击右上角的Debug with native code,调试DMP文件。

当然,还可以开启变量监视、即时窗口等来查看相关变量的值(比如这里我需要在监视里查看map的数据),其实和正常使用VS进行调试别无二致,VS真香,详细可以参考上述博客;

然后发现问题了,就是本文开篇所说的,拿到锁的线程卡在了68行的TryToGetQueryTree()里面,而具体的就是卡在了该函数函数体中的map.find()里面(由于代码优化掉了,用VS堆栈只能看到TryToGetQueryTree(),可以用Windbg查看更深的堆栈调用,

对于本文就是通过Windbg查看堆栈看到了是卡在map.find()这里),而执行不到第76行(释放锁);其他的两个线程在65行出等待锁,这样程序就一直运行,线程T2陷入死循环一直运行。

  • 那么我们看下问题map的数据

竟然有重复的数据!!map里面的元素的key应该是唯一的!!

再看下map数据结构中的值,

看到了没?节点17.g:447的left指向了节点17.g:449,而节点17.g:449的left又指向了节点17.g:447,这样无限循环的指下去,而根据VS堆栈变量可以看到map.find()出现死循环时候参数是17.g:447,也就是在map中查找

17.g:447的元素时候发生了死循环,一直出不来,那么结合map.find()的源码推理重现该问题,参考c++ map find方法源码解析

2.4.问题

至此,已经发现问题了,就是map数据出问题了,map中元素的key出现了重复,并且节点17.g:447的left指向了节点17.g:449,而节点17.g:449的left又指向了节点17.g:447,这样无限循环的指下去,

这样导致了map.find(17.g:447)的时候陷入死循环,进而导致程序一直运行不退出,像发生了“死锁”一样。

3.解决方案

后续更新:此问题本质原因是使用多线程发生了资源同步问题:同时多个线程写(更改)map缓存,导致map树节点出现了循环指向的问题。后续优化线程资源同步机制后,保证同时只有一个线程写(更改)map缓存,此问题解决。

为什么map中数据会异常?出现key重复的元素,map本身构建红黑树的过程出问题可能性很小,而比较器是我定制的,应该是比较器除了问题,比较器很简单就是调用strcmp来实现的,

struct ptrStrLess
{
    bool operator()(char const* a, char const* b) const
    {
        return strcmp(a, b) < 0;
    }
};

那么分析应该是strcmp()出问题了,查阅相关资料,

  • strcmp应该没有问题,但是类似的函数比如strcpy等就有一定的安全问题,微软推荐使用更安全的版本strcpy_s,而strcmp也有另外一个版本strncmp,但是确实没有资料证明strcmp会有问题...但我遇到的问题又推测strcmp最有可能出问题...
  • VS编译时候设置编译优化O2的话会将字符串优化,导致字符串地址共享(The additional options applied by /O1 and /O2 can cause pointers to strings or to functions to share a target address, which can affect debugging and strict language conformance. /Ox 选项是在不包括和的情况下启用大多数优化的简单方法 /GF /Gy 。),详细可参见/Ox (Enable Most Speed Optimizations)
  • 也有其他人使用map时候出现奇怪问题的情况,而且多是在多线程环境中,如std::map.find()崩溃怪事回调函数里使用map导致程序崩溃

至此,问题应该是出在strcmp(),解决方案有几种:

  • 仍使用const char*作为map的key,但修改比较器的实现:自己写一个strcmp函数、使用strlen和strncmp来实现strcmp、构造string对象并使用string.compare接口实现比较器;
  • 使用string替换const char*作为map的key(这样不用定制比较器了);

最后我才用的第二种方案,即使用string替换const char*作为map的key,原因不再展开(使用其他方案依然有不同的问题发生,怀疑是c字符串使用有坑),并且经过测试替换后速度比原来慢一点(一个测试文件原来是2分15秒,替换后为2分17秒),但尚可接受。

当然判断采用的方案到底有没有效?具体问题是不是strcmp?还是得能重现问题才可以,经用相同文件相同环境实测几次没有重现map数据发生异常的情况,可判断该问题可能是微软的bug,先处理后部署到线上再看后续还有没有这种问题吧。

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值