场景
今天在看大名鼎鼎的muduo时,看到作者讨论一个问题:
1)一个类Stock
,每个对象都有唯一的一个key作为标识,对象会不断更新和被多处共享。
2)如果一个key没有被任何地方用到,应该析构它对应的对象,释放资源
实现一个类StockFactory
的接口get(const string& key)
,查找集合map
中是否存在key绑定的对象,如果不存在则创建一个,最后返回Stock对象。
问题
- 若使用智能指针
shared_ptr
强绑定,返回类Stock
的对象,只要有一个指向该对象的shared_ptr
存在,对象就无法析构(map
中存有{ key, shared_ptr<Stock }
); - 若使用
weak_ptr
的引用管理shared_ptr<Stock>
,然后使用lock
成员函数获取所管理对象的强引用,返回这个指针,会解决对象无法析构的问题。但是会有轻微的内存泄漏,因为map集合会无限地增长,而不会减少。
代码实现
shared_ptr<Stock> StockFactory::get(const string& key) { shared_ptr<Stock> pStock; /*要返回的指针*/ MutexLockGuard lock(mutex_); weak_ptr<Stock>& wkStock = map[key]; /*用weak_ptr管理key对应的shared_ptr*/ pStock = wkStock.lock(); /*将weak_ptr提升'强'绑定*/ /*如果map中不存在key,wkStock得到的是默认构造的,需要生成一个*/ if (!wkStock) { pStock.reset(new Stock(key)); wkStock = pStock; /*wkStock是引用类型的,对其修改会同步更新map[key]*/ } return pStock; }
解决办法
定制析构
shared_ptr
有一个定制析构功能,传入一个函数指针或仿函数在析构对象时执行d(ptr)
。
class StockFactory { public: shared_ptr<Stock> StockFactory::get(const string& key) { ... if (!wkStock) { pStock.reset(new Stock(key), boost::bind(&StockFactory::deleteStock, this, -1)); /*线程不安全*/ wkStock = pStock; /*wkStock是引用类型的,对其修改会同步更新map[key]*/ } private: void deleteStock(Stock* stock) { if (stock) { MutexLockGuard lock(mutex_); /*存在竞态,在陈硕知乎专栏中能看到详细讲解*/ stocks_.erase(stock->key()); } delete stock; } mutable MutexLock mutex_; unordered_map<string, shared<Stock>> map; };
竞态发生的情况在于,进入deleteStock
函数后,执行erase
之前,如果别的线程调用了此函数,把map中的key删掉,创建了一个新的key对应的对象(reset
),那么最终会产生两个key对应的对象,违背对象池语义。
/*boost::bind*/
处把对象指针绑定到boost::function()
中会存在线程安全的问题。如果StockFactory
先于对象stock
析构,那么stock
对象已经不存在了再去调用其析构函数,就会core dump
。书的作者给出的解决办法是采用弱回调技术。
线程安全
在1.11.1
小节中,介绍了一个把this
指针转换为shared_ptr
的方法,使用shared_from_this
,这样在析构stock
对象之前不会StockFactory
不会被析构。但是这样会延长StockFactory
的生命周期。
- 代码——shared_from_this
/*修改后的版本*/ pStock.reset(new Stock(key), boost::bind(&StockFactory::deleteStock, shared_from_this(), -1));
想实现 “如果对象还活着,就调用其成员函数,否则就忽略” 这样的情况,可以用weak_ptr代替之。
流程是先尝试将this
提升为shared_ptr
,然后强制转化为weak_ptr
。若提升成功,说明对象还存在,执行回调函数;若转化失败,那么对象已经被析构,就不用考虑线程安全的问题了。 - 代码——weak_ptr(shared_from_this())
pStock.reset(new Stock(key), boost::bind(&StockFactory::deleteStock, boost::weak_ptr<StockFactory>(shared_from_this()), -1));
总结
分析race condition是多线程编程的基本功,需要多练习以及多积累教训。作者给出建议是: - 尽量不要使用跨线程的对象,使用流水线,生产者消费者,任务队列这样有规律的机制,减少数据共享,是最好的多线程编程的建议。*