【问题标题】:Crash during erase from map从地图擦除时崩溃
【发布时间】:2021-06-21 15:01:12
【问题描述】:

由于意外行为,我发生了崩溃。我想问一下如何修改代码以防止发生的崩溃。

   void SubscManag::handleNotif(Notif notif)
    {
   
        std::cout << "Current subscribe size: " << subs_.size();
        std::map<int, SubscData>::iterator it;
        {
            std::lock_guard<std::recursive_mutex> lock(mutex_);
            it = subs_.begin();
        }
        for (;;)
        {
            std::lock_guard<std::recursive_mutex> lock(mutex_);
            if (it == subs_.end())
            {
                break;
            }
            if (it->second.hasToBeErased)
            {
                std::cout << "Erase id : " << it->first;
                it = subs_.erase(it);
                continue;
            }
            if (cond)
            {
                // ....
                ++it;
                continue;
            }
            // ....
            ++it;
        }
    }

崩溃发生在 it = subs_.erase(it); 有了这个

 Erase id : 2
 Erase id : 3
 Erase id : 28473456

所以Element with id : 28473456 可能不存在或已损坏,但我该如何防御崩溃? 谢谢。

【问题讨论】:

  • 我无法复制它
  • 如果你不能用完整的代码重现它,你希望我们如何只用部分代码?请提供minimal reproducible example。我的完整猜测是您在多个线程上对handleNotif 的调用重叠,其中一个线程使另一个线程的迭代器无效
  • 我只是在寻求建议|如何让这部分代码更安全。很明显,在 subs_ 中没有插入任何 28473456 id
  • 您的第一个锁是“错误的”,因为it 在进入循环之前可能会失效。

标签: c++ c++11 crash segmentation-fault c++14


【解决方案1】:

你正在尝试做基于锁的并发。

基于锁的并发不构成。本地正确的操作在连接时会产生垃圾。

这里的问题是你的it是在一个锁中生成的,然后你解锁。在解锁期间,it 可能有效也可能无效。然后你重新锁定并假设it 仍然有效。

您要么必须锁定您在地图上工作的整个时间段,然后解锁它并忘记锁定中您所知道的几乎所有内容(不携带迭代器),根据不可变数据结构重写您的代码,或者执行其他更激进的东西。

{
  std::lock_guard<std::recursive_mutex> lock(mutex_);
  it = subs_.begin();
}

你在这里锁定,做一个操作,然后......在作用域的末端解锁互斥锁。

所以it 是映射到地图的迭代器,当您持有它时,您不会阻止其他线程编辑地图。这可能会使it 无效。

同样的事情发生在你的for(;;) 循环中;您在每次迭代开始时锁定,然后在结束时解锁。 it 引用的地图元素可能会被删除,从而使您的 it 在一次迭代和下一次迭代之间失效。

这是一个简单的实用程序类:

template<class T>
struct mutex_guarded {
  template<class F>
  auto read(F&& f)const {
    auto l = std::unique_lock<std::mutex>(m);
    return f(t);
  }
  template<class F>
  auto write(F&& f) {
    auto l = std::unique_lock<std::mutex>(m);
    return f(t);
  }
private:
  mutable std::mutex m;
  T t;
};

现在,将您的 std::map&lt;int, SubscData&gt; 更改为 mutex_guarded&lt;std::map&lt;int, SubscData&gt;&gt;

下一步:

void SubscManag::handleNotif(Notif notif)
{
  subs_.write([&](auto& subs_) {
    std::cout << "Current subscribe size: " << subs_.size();
    auto it = subs_.begin();
    for (;;)
    {
        if (it == subs_.end())
        {
            break;
        }
        if (it->second.hasToBeErased)
        {
            std::cout << "Erase id : " << it->first;
            it = subs_.erase(it);
            continue;
        }
        if (cond)
        {
            // ....
            ++it;
            continue;
        }
        // ....
        ++it;
      }
  });
}

您将在访问 subs_ 时锁定互斥锁。

请注意,通过其他方法访问subs_ 仍然很危险。使用互斥锁保护单个方法调用是一个糟糕的计划。让“这个对象可以从任何线程访问”是 999/1000 案例中的灾难,通常会延迟,最初的小案例工作,然后在编写代码很长时间后,它会在死锁和竞争条件下爆炸。

mutex_guarded 不是最好的解决方案,但它比按方法锁定要好。

【讨论】:

  • 只需在handleNotif 的开头调用std::lock_guard&lt;std::recursive_mutex&gt; lock(mutex_);(并删除其他两个)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-15
  • 2022-01-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多