[关闭]
@lunar 2016-11-15T10:03:18.000000Z 字数 2127 阅读 3831

记一次诡异的bad_alloc问题

程序员的自我修养 备忘


遇到一个难查的bug,记下来备忘。

问题描述&解决方案

问题最早是这样,一个分布式多线程LDA程序,运行时报bad_alloc,查了这个异常是内存分配失败。free -h查看机器上内存使用情况,发现一般都有很大余量。而且这个异常抛出的时机不固定,有可能是在迭代30+的时候报错,也有可能迭代100+报错,有时200迭代完也不报错顺利结束。由于是分布式跑的,所以先单机跑了,发现没有问题,因此问题不出在分布式实现上。
一开始debug时候只能在很多块打log然后加try/catch,这么做太蠢了,然后就去现学了下gbd的用法。然后发现会在如下代码中陷入死循环:

  1. std::vector<int> get_keys(){
  2. std::vector<int> keys;
  3. for(auto it=count_v_.begin(); it!=count_v_.end(); ++it){
  4. //陷在该循环中出不去了
  5. //这里的count_v_是一个std::unordered_map<int,T>
  6. int v = it->first;
  7. keys.push_back(v);
  8. }
  9. return keys;
  10. }

ok,定位了出问题的代码,那么来看下是什么问题。在该循环中不断获得v,不断插入keys中,直到内存分配完。那么为什么会出现这种情况呢? 我修改了代码,让几个变量keys,count_v_输出长度,然后加了个把keys里的值全都加入一个set,一起看下长度。输出如下:

  1. count_v_.size() = 10534
  2. keys.size() = 10723
  3. keyset.size() = 10501

这个就很奇怪了,接着发现count_v_这个map中有的map[key].count等于0,这部分key也就是说不算在size之中的,但是遍历的时候会经过(没有看具体的STL实现,应该是这样)。
这时候google出这么一条线索

向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的迭代器失效,一个失效的迭代器将不再表示任何元素,使用失效的迭代器是一种严重的程序设计错误,很可能引起与使用未初始化指针一样的问题。
...
unordered_[multi]{set,map}:如果插入操作不导致扩大存储空间,进而重新 hash,那么迭代器不受影响,否则所有的迭代器失效。

那么应该就是迭代器失效了,但是这段代码里面没有删除或者插入操作啊,而且此时程序已经结束多线程处理,没有其他线程能操作这个变量了啊,这时候就陷入了困境,纠结了好久。
后来在真真提醒下,我想到,如果在进入这段代码之前就map就已经损坏了呢?就已经出了问题呢?
STLcontainer设计的时候保证:

STL containers are designed so that you are guaranteed to be able have:
A: Multiple threads reading at same time, or
B: One thread writing at the same time

这个count_v_就是多线程构建的,那么会不会是构建的时候出问题呢?
构建代码如下:

  1. CounterMap& get_counter(int v) {
  2. return count_v_[v];
  3. }

其实就一行,如果v不在count_v_中,那么访问时会调用CounterMap的默认构造函数来给count_v_[v]设一个默认value。虽然代码里面保证了每个线程处理的v不同,但是count_v_这是个unordered_map,是个哈希表,也就是说有可能hash(v)撞车而导致这个容器给搞坏从而引起后面的问题(具体怎么搞坏还是要看STL实现,很有可能是迭代器获取M_next的时候会陷入一个环)。
那么也就是要防止多线程同时操作count_v_,加锁试试看。所以就把如上代码修改如下:

  1. CounterMap& get_counter(int v) {
  2. if(count_v_.find(v) != count_v_.end())
  3. return count_v_[v];
  4. else{
  5. //mt 声明在类的private成员中 std::mutex mt;
  6. mt.lock();
  7. auto tmp = count_v_[v];
  8. mt.unlock();
  9. return count_v_[v];
  10. }
  11. }

测试,搞定。

感想

这次debug花了大概有一天半的时间(顺手还修了另一个bug)。过程相当纠结,不过也同时慢慢开始点了gdb调试,dump core,多线程操作,容器的一部分知识等技能(虽然很多都只是皮毛)。
说到底,这个还是线程安全的锅,并发程序确实不好写,这也是我第一次接触多线程代码(没学过操作系统的人啊。。),这方面还得好好补补课。
其实感觉debug就像是侦探破案一样,需要先找到线索(不符合预期的行为),顺着线索追查,尽量还原犯罪现场(gdb调试复现/load core复现),有时要通过线人获得情报(出错代码段try/catch),翻阅往年卷宗(google有没有人遇到同样问题),看穿嫌疑人的不在场证明(这里虽然是在遍历map时报异常,但是其实问题发生在多线程构建期间)。这么看的话debug 还挺有意思的(雾)。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注