场景

今天在看大名鼎鼎的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是多线程编程的基本功,需要多练习以及多积累教训。作者给出建议是:
  • 尽量不要使用跨线程的对象,使用流水线,生产者消费者,任务队列这样有规律的机制,减少数据共享,是最好的多线程编程的建议。*