对象池的一个 race condition

拙作《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 重新实现,贴在这里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
复制代码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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码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 万册。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%