拙作《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 | 复制代码class Stock : boost::noncopyable |
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 | 复制代码void deleteStock(Stock* 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 万册。
本文转载自: 掘金