shared_ptr和weak_ptr

最近在读某书,发现自己有点儿落伍,c++的标准太老旧,以前各项目的代码基本都是裸指针。裸指针带来的很多的问题,也相当的头疼。

近两年主要在写golang,golang用起来的确很顺手,带GC可以减轻很多思想上的包袱。后来仔细一想,其实包袱主要减轻在了资源的管理上面。在golang中创建一个对象,然后到处引用,完全不用考虑该对象何时释放,释放后其它引用到的地方该怎样进行清理。的确资源管理是旧标准c++上最难处理的事情之一,写过大型c++项目的人应该深有体会。

比如在我的游戏中,每个怪物会定时搜索地图上的攻击目标,搜索到何时的目标后,会将自己的target指向目标。逻辑很简单,但是在这几年中,target死亡后把自己delete了,但是有该target的对象依旧引用着它,导致在处理攻击逻辑的时候直接崩溃了。然后就是各种现在看来很简陋或者麻烦的方法来处理该问题:遍历当前场景下所有object,当object为自己的时候进行清理。虽然算是解决了这个问题,但是实现的很不优雅。在大概学习和实践了shared_ptr和weak_ptr后,发现这两个东西解决上面的问题实在是太简单了。

简介

.shared_ptrweak_ptr属于智能指针,shared_ptr用于通过引用计数管理对象的生命周期,而weap_ptrshared_ptr配对使用来避免交叉引用。

.shared_ptr可以复制,当被复制后,则引用计数+1,当引用计数为0的时候释放对象。而weak_ptr可以被shared_ptr赋值,但是不会增加引用计数。当weak_ptr使用的时候,必须通过lock方法来获得shared_ptr来操作指向的对象。

可能有朋友会想到个问题,weak_ptr不增加引用计数,那么当之前的shared_ptr生命周期完结释放了管理的指针,那么weak_ptr不就成为了管理野指针的智能指针了么?

所以上面写了,weak_ptr的使用得通过lock来获得shared_ptr,假设对象已经被释放了,那么获得的shared_ptr就是空,所以不存在上面的问题。

弱回调

由于是自己的笔记,很多细节都省略了。这儿直接说shared_ptr的另一个特性,就是可以指定释放函数,也就是在被管理的对象由于引用计数为0后析构前,不会调用相应的析构函数,而是直接调用传入的释放函数。这个有个好处,比如可以实现对象池。

下面是一个简单的代码例子,详细在我的github中。

class WeakItem {
public:
    WeakItem() {
        value_ = 0;
    }
    ~WeakItem() {
        printf("delete item key %s\r\n", key_.c_str());
    }

public:
    int value_;
    std::string key_;
};

class WeakFactory : public std::enable_shared_from_this<WeakFactory>
{
public:
    WeakFactory() {

    }
    ~WeakFactory() {
        printf("delete factory, left item %d\r\n", items_.size());
    }

public:

    std::shared_ptr<WeakItem> get(std::string key) {
        lock_.lock();
        auto& wptr = items_[key];
        auto sptr = wptr.lock();
        if (nullptr == sptr) {
            // Not allocated
            sptr = std::shared_ptr<WeakItem>(new WeakItem, 
                std::bind(&WeakFactory::free_item, 
                std::weak_ptr<WeakFactory>(shared_from_this()), 
                std::placeholders::_1));
            sptr->value_ = 0;
            sptr->key_ = key;
            // To weak_ptr
            wptr = sptr;
        }
        lock_.unlock();

        return sptr;
    }

public:
    static void main(int argc, char* argv[]) {
        printf("%s start\r\n", __FUNCTION__);
        std::shared_ptr<WeakItem> powned_item;
        std::weak_ptr<WeakFactory> pwfac;

        {
            // Factory scope
            std::shared_ptr<WeakFactory> pfac(new WeakFactory);
            pwfac = pfac;

            powned_item = pfac->get("owned");

            auto tfunc = [pfac]() {
                for (int i = 0; i < 100; i++) {
                    char buf[8] = {0};
                    sprintf(buf, "%d", i);
                    std::string key = buf;
                    auto item = pfac->get(key);
                    // We do not use it any more out of this scope,
                    // So item will be released by shared_ptr immediately
                }
            };
            std::thread t1(tfunc);
            std::thread t2(tfunc);

            t1.join();
            t2.join();
        }

        auto pfac = pwfac.lock();
        if (nullptr == pfac) {
            printf("factory already deleted\r\n");
        } else {
            printf("factory not deleted\r\n");
        }
        // Factory is deleted, owned_item should be delete but not erase from factory
        // ...
        printf("%s end\r\n", __FUNCTION__);
    }

private:

    void delete_from_internal(WeakItem* old) {
        lock_.lock();
        items_.erase(old->key_);
        lock_.unlock();
    }

    // We declare a static function to release the WeakItem
    static void free_item(std::weak_ptr<WeakFactory> ptr, WeakItem* old) {
        auto sptr = ptr.lock();
        if (nullptr != sptr) {
            sptr->delete_from_internal(old);
        } else {
            printf("factory is deleted ..., item %s not erased from items_\r\n",
                old->key_.c_str());
        }

        delete old;
        old = nullptr;
    }

private:
    // Save as weak_ptr rather than shared_ptr
    // If we use the shared_ptr, items_ will always hold the shared_ptr
    // and items referenced won't be deleted by shared_ptr
    std::map<std::string, std::weak_ptr<WeakItem> > items_;
    std::mutex lock_;
};

上述代码实现了一个对象池,线程安全并且没有内存泄漏。下面谈几个细节:

  1. 工厂类的存储item的类型为weak_ptr

这个应该比较好理解。我们在get中返回了shared_ptr,假设我们又在工厂内存了相同类型,则该item永远不会被释放(至少在factory销毁前不会被释放)。改用weak_ptr可以解决这个问题。

  1. shared_ptr的释放函数

在上面的例子中,我们可以看到创建shared_ptr的时候是这样的:

sptr = std::shared_ptr<WeakItem>(new WeakItem, 
                    std::bind(&WeakFactory::free_item, 
                    std::weak_ptr<WeakFactory>(shared_from_this()), 
                    std::placeholders::_1));

我们制定了一个释放函数,用处在于在shared_ptr销毁管理对象的时候,我们可以把该对象从factory中移除。这儿的细节比较多,首先,我们看到了shared_from_this

shared_from_this用于获得this指针的shared_ptr,而获得这种能力的方式是在类必须继承std::enable_shared_from_this<WeakFactory>,还有一点是该类必须由shared_ptr所管理。

当然由于factory可能先于item销毁,所以我们将回调中的shared_ptr又转为了weak_ptr,在回调中我们判断是否资源有效,假设无效就不会从工厂里进行销毁。在这里,释放函数的形参虽然是weak_ptr,但是std::bind是值复制,还是会复制shared_ptr,然后在调用的时候再进行转换,也就是说factory的shared_ptr又有一个存在了闭包中。只有该闭包被执行并销毁后才会释放factory,也就是该item的释放函数必须得调用后才行,那么就是所有的item被释放后才会释放factory。为了让factory的生命周期不进行延长,我们做了上述的工作。

线程安全

首先,shared_ptr不是线程安全的,指的是多线程操作同一个share_ptr必须有自己的同步措施。而多个线程拥有同一个shared_ptr的拷贝则是安全的,在多线程下该拷贝析构也是安全的,所以在实践上来说,每个线程必须得有一个shared_ptr的拷贝。大概流程如下:

  1. 线程1初始化创建对象A的shared_ptr s1。
  2. 对s1进行加锁,线程2拷贝至自己的s2。

这样每个线程都有自己的shared_ptr了,至于每个智能指针的销毁则是安全的。

解决一开始的问题

这儿就比较的简单了。我们可以将怪物的target定义为weak_ptr,当target被销毁的时候,weak_ptr的lock为空,则可以得知指向的对象已经销毁了,解决这个就是这么简单。

共 0 条回复
暂时没有人回复哦,赶紧抢沙发
发表新回复

作者

sryan
today is a good day