122

对象池的一个 race condition

 6 years ago
source link: https://zhuanlan.zhihu.com/p/30522095?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

对象池的一个 race condition

C++等 2 个话题下的优秀答主

拙作《Linux 多线程服务端编程》第 1.11 节介绍了如何用 shared_ptr/weak_ptr 实现对象池,最近有读者指出对象销毁有 race condition。本文介绍一下复现及修复的方法。

从第 22 页的 version 3 开始的代码有这个 race condition,包括第 1.11.1 节的 version 4 和第 1.11.2 节的弱回调版,见试读样张,配套代码见 GitHub。这个 race condition 再次验证了对象的销毁比创建更难。

Race condition

为了突出重点,本文以 version 3 为例,介绍 race condition 的成因及修复方法,完整代码(包括修复)见 GitHub。为了便于下文讨论,我把 version 3 代码的代码用 C++11 重新实现,贴在这里。

class Stock : boost::noncopyable
{
 public:
  Stock(const string& name)
    : name_(name)
  {
  }

  const string& key() const { return name_; }

 private:
  string name_;
};

// 对象池
class StockFactory : boost::noncopyable
{
 public:

  std::shared_ptr<Stock> get(const string& key)
  {
    std::shared_ptr<Stock> pStock;
    muduo::MutexLockGuard lock(mutex_);
    std::weak_ptr<Stock>& wkStock = stocks_[key];
    pStock = wkStock.lock();
    if (!pStock)
    {
      pStock.reset(new Stock(key),
                   [this] (Stock* stock) { deleteStock(stock); });
      wkStock = pStock;
    }
    return pStock;
  }

 private:

  void deleteStock(Stock* stock)
  {
    if (stock)
    {
      muduo::MutexLockGuard lock(mutex_);
      stocks_.erase(stock->key());
    }
    delete stock;
  }

  mutable muduo::MutexLock mutex_;
  std::unordered_map<string, std::weak_ptr<Stock> > stocks_;
};

Race condition 发生在 StockFactory::deleteStock() 这个成员函数里,如果进入 deleteStock 之后,在 lock 之前,有别的线程调用了相同 key 的 StockFactory::get(),会造成此 key 被从 stocks_ 哈希表中错误地删除,因此会重复创建 Stock 对象。程序不会 crash 也不会有 memory leak,但是程序中存在两个相同 key 的 Stock 对象,违背了对象池应有的语意。下图描绘了 race condition 的发生过程。

这个 race condition 可以用 sleep() 很容易地复现出来,见 GitHub 上的代码,编译时须定义 REPRODUCE_BUG 这个宏。

修复这个 race condition 的办法很简单,在 deleteStock() 中,拿到 lock 之后,检查一下 weak_ptr 是否 expired(),然后只在 expired() 为 true 的情况下从 stocks_ 中删掉 key。

void deleteStock(Stock* stock)
  {
    if (stock)
    {
      muduo::MutexLockGuard lock(mutex_);
      auto it = stocks_.find(stock->key());
      assert(it != stocks_.end());
      if (it->second.expired())
      {
        stocks_.erase(it);
      }
    }
    delete stock;
  }

修复之后,原来的 race condition 不复存在:

如果把条件 if (it->second.expired()) 改成 if (!it->second.lock()),即试着将 weak_ptr 提升为 shared_ptr,如果提升不成功,则 erase key,这样做有没有问题?

这样做有可能造成死锁,因为 muduo Mutex 是不可重入的。race condition:如果 weak_ptr::lock() 成功,拿到一个 shared_ptr (use_count 应该 > 1),然后在此 shared_ptr 析构之前,其他线程释放了这个对象,使得 use_count 降为 1,那么当此 shared_ptr 析构的时候,会递归调用 deleteStock(),从而造成死锁。

Herb Sutter 在 CppCon2016 上也提到了类似的对象池技术,他的实现对应书中的 version 2,没有这个 race condition,但对象池的大小只增不减。演讲视频:My CppCon talk video is online,幻灯片:CppCon/CppCon2016

承蒙读者厚爱,《Linux 多线程服务端编程》自从 2013 年 1 月面世以来,截至 2017 年 11 月,累计印刷 10 次,印数共 2 万册。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK