【问题标题】:Locking a shared_ptr锁定 shared_ptr
【发布时间】:2011-11-09 23:15:33
【问题描述】:

我有一个共享对象需要发送到系统 API 并稍后将其提取回来。系统 API 仅接收 void *。我不能使用 shared_ptr::get() 因为它不会增加引用计数,并且它可以在从系统 API 中提取之前被其他线程释放。发送一个新的 shared_ptr * 将起作用,但涉及额外的堆分配。

一种方法是让从enable_shared_from_this派生的对象。但是,因为这个类模板只拥有一个weak_ptr,所以还不足以阻止该对象被释放。

所以我的解决方案如下所示:

class MyClass:public enable_shared_from_this<MyClass> {
private:
    shared_ptr<MyClass> m_this;
public:
    void *lock(){
        m_this=shared_from_this();
        return this;
    }
    static shared_ptr<MyClass> unlock(void *p){
        auto pthis = static_cast<MyClass *>(p);
        return move(pthis->m_this);
    }
/* ... */
}

/* ... */
autp pobj = make_shared<MyObject>(...);
/* ... */
system_api_send_obj(pobj->lock());
/* ... */
auto punlocked = MyClass::unlock(system_api_reveive_obj());

有没有更简单的方法来做到这一点?

此解决方案的缺点:

  • 除了基类enable_shared_from_this 中的weak_ptr 之外,它还需要在MyClass 对象布局中添加一个shared_ptr&lt;MyClass&gt;

  • 正如我在 cmets 中提到的,同时访问 lock()unlock() 是不安全的。

  • 最糟糕的是这个解决方案在调用unlock()之前只能支持一次lock()。如果同一对象要用于多个系统 API 调用,则必须实现额外的引用计数。

如果我们有另一个enable_lockable_shared_from_this 课程,那就更好了:

class MyClass:public enable_lockable_shared_from_this<MyClass> {
/* ... */
}

/* ... */
autp pobj = make_shared<MyObject>(...);
/* ... */
system_api_send_obj(pobj.lock());
/* ... */
auto punlocked = unlock_shared<MyClass>(system_api_reveive_obj());

enable_lockable_shared_from_this的实现与enable_shared_from_this类似,唯一的区别是它实现了lock()和一个辅助函数unlock_shared。这些函数的调用只会显式地增加和减少 use_count()。这将是理想的解决方案,因为:

  • 它消除了额外的空间成本

  • 它重用了 shared_ptr 现有的设施来保证并发安全。

  • 此解决方案的最佳之处在于它无缝支持多个lock() 调用。

不过,唯一最大的缺点是:暂时不可用!

更新:

这个问题至少有两个答案涉及指针容器。请将这些解决方案与以下解决方案进行比较:

class MyClass:public enable_shared_from_this<MyClass> {
private:
    shared_ptr<MyClass> m_this;
    mutex this_lock; //not necessory for single threading environment
    int lock_count;
public:
    void *lock(){
        lock_guard lck(this_lock); //not necessory for single threading environment
        if(!lock_count==0)
            m_this=shared_from_this();
        return this;
    }
    static shared_ptr<MyClass> unlock(void *p){
        lock_guard lck(this_lock); //not necessory for single threading environment
        auto pthis = static_cast<MyClass *>(p);
        if(--lock_count>0)
            return pthis->m_this;
        else {
            lock_count=0;
            return move(pthis->m_this); //returns nullptr if not previously locked
        }
    }
/* ... */
}

/* ... */
autp pobj = make_shared<MyObject>(...);
/* ... */
system_api_send_obj(pobj->lock());
/* ... */
auto punlocked = MyClass::unlock(system_api_reveive_obj());

这绝对是一个 O(1) vs O(n)(空间;时间是 O(log(n)) 或类似,但绝对大于 O(1))的游戏。

【问题讨论】:

  • “更简单的方法”?该解决方案是否有效?指向 API 函数的 MyClass 指针有什么用?基本上,控制system_api_set_key()system_api_get_key() 的人应该保持一个共享指针,以便适当地管理对象的生命周期。
  • 这是有效的,因为对象在锁定时拥有对自身的强引用,并且这是一个循环引用,直到 m_this 被释放。 system_api_send_key(对不起,我改了名字)和system_api_receive_key是SYSTEM API,我不能改。
  • 我唯一需要提的是,多线程同时访问 m_this 是不安全的。 shared_ptr 保证对引用对象的多线程访问是安全的,但不是 shared_ptr 本身。所以我们需要围绕lockunlock 函数调用进行一些锁定。
  • 某些 API 允许您将 void * 作为“句柄”传递,只有客户端才能理解;系统只是将其存储在某个地方并在将来返回。是的,代码与 C++11 中的shared_ptr&lt;MyObject&gt; tmp=m_this; m_this.reset(); return tmp; 相同。移动构造函数保证上述内容。
  • @Vaughn :通过 PostMessage 和 GetMessage 传递的消息是整数,具体大小与指针大小相同。即,在这种情况下,计划将指针作为整数传递是完全可以的。

标签: c++ c++11 shared-ptr


【解决方案1】:

我现在有以下想法:

template<typename T>
struct locker_helper{
    typedef shared_ptr<T> p_t;
    typedef typename aligned_storage<sizeof(p_t), alignment_of<p_t>::value>::type s_t;
};
template<typename T> void lock_shared(const shared_ptr<T> &p){
    typename locker_helper<T>::s_t value;
    new (&value)shared_ptr<T>(p);
}
template<typename T> void unlock_shared(const shared_ptr<T> &p){
    typename locker_helper<T>::s_t value
        = *reinterpret_cast<const typename locker_helper<T>::s_t *const>(&p);
    reinterpret_cast<shared_ptr<T> *>(&value)->~shared_ptr<T>();
}


template<typename T>
void print_use_count(string s, const shared_ptr<T> &p){
    cout<<s<<p.use_count()<<endl;
}

int main(int argc, char **argv){
    auto pi = make_shared<int>(10);
    auto s = "pi use_count()=";
    print_use_count(s, pi); //pi use_count()=1
    lock_shared(pi);
    print_use_count(s, pi);//pi use_count()=2
    unlock_shared(pi);
    print_use_count(s, pi);//pi use_count()=1
}

然后我们就可以实现原来的例子如下:

class MyClass:public enable_shared_from_this { /*...*/ };

/* ... */
auto pobj = make_shared<MyClass>(...);
/* ... */
lock_shared(pobj);
system_api_send_obj(pobj.get());
/* ... */
auto preceived = 
    static_cast<MyClass *>(system_api_reveive_obj())->shared_from_this();
unlock_shared(preceived);

用这个想法很容易实现enable_lockable_shared_from_this。但是,上面更通用,允许控制不是从 enable_lockable_from_this` 模板类派生的东西。

【讨论】:

  • 这与我的想法一致。不过,我看到了几个问题,这就是我没有发布它的原因:unlock_shared 中的 reinterpret_cast 是,AFAIK,要么是实现定义的,要么是未定义的。如果是后者,解决方案对我来说是不可行的,如果是前者,至少不是可取的,但如果您没有其他选择,请好好记录并使用它。另一个问题是,如果您的 API 从不执行回调,因为您人为地增加了引用计数,它就永远不会被释放。
  • 您使用堆栈溢出错误。仅发布答案或编辑您的问题,但不要发布问题的答案。即,堆栈溢出不使用优雅的论坛风格发帖,OP 始终是一个问题,所有其他帖子都是答案。
  • 我同意存在reinterpret_cast 不可移植的问题。这个问题只能由 shared_ptr 作者提供enable_lockable_shared_from_this 之类的东西来解决。 API问题不是问题,因为我们总是假设API是正确的,否则我们将提交错误报告。
  • @phresnel 我已经删除了这个问题,现在它是一个答案。
  • 我看到的问题是,在unlock_shared 中,您正在复制shared_ptr 的内存,就像您执行memcpy 一样,然后您在该副本中调用析构函数。但这是 UB,因为 shared_ptr 类不可轻易复制。
【解决方案2】:

通过调整之前的答案,我终于得到了如下解决方案:

//A wrapper class allowing you to control the object lifetime
//explicitly.
//
template<typename T> class life_manager{
public:
    //Prevent polymorphic types for object slicing issue.
    //To use with polymorphic class, you need to create
    //a container type for storage, and then use that type.
    static_assert(!std::is_polymorphic<T>::value, 
        "Use on polymorphic types is prohibited.");

    //Example for support of single variable constructor
    //Can be extented to support any number of parameters
    //by using varidict template.
    template<typename V> static void ReConstruct(const T &p, V &&v){ 
        new (const_cast<T *>(&p))T(std::forward<V>(v));
    }

    static void RawCopy(T &target, const T &source){
        *internal_cast(&target) = *internal_cast(&source);
    }
private:
    //The standard said that reterinterpret_cast<T *>(p) is the same as 
    //static_cast<T *>(static_cast<void *>(p)) only when T has standard layout.
    //
    //Unfortunately shared_ptr do not.
    static struct { char _unnamed[sizeof(T)]; } *internal_cast(const T *p){
        typedef decltype(internal_cast(nullptr)) raw;
        return static_cast<raw>(static_cast<void *>(const_cast<T *>(p)));
    }
};

//Construct a new instance of shared_ptr will increase the reference
//count. The original pointer was overridden, so its destructor will
//not run, which keeps the increased reference count after the call.
template<typename T> void lock_shared(const std::shared_ptr<T> &p){
    life_manager<shared_ptr<T> >::ReConstruct(p, std::shared_ptr<T>(p));
}

//RawCopy do bit-by-bit copying, bypassing the copy constructor
//so the reference count will not increase. This function copies
//the shared_ptr to a temporary, and so it will be destructed and
//have the reference count decreased after the call.
template<typename T> void unlock_shared(const std::shared_ptr<T> &p){
    life_manager<shared_ptr<T> >::RawCopy(std::shared_ptr<T>(), p);
}

不过,这实际上与我之前的版本相同。我唯一做的就是创建一个更通用的解决方案来显式控制对象的生命周期。

根据标准(5.2.9.13),static_cast 序列肯定是明确定义的。此外,“原始”复制行为可能未定义,但我们明确要求这样做,因此用户必须在使用此工具之前检查系统兼容性。

此外,在这个例子中实际上只需要RawCopy()ReConstruct的介绍仅作一般用途。

【讨论】:

    【解决方案3】:

    为什么不直接将void * API 封装在一个跟踪该 API 对您的对象的引用的生命周期的东西中?

    例如。

    typedef std::shared_ptr<MyClass> MyPtr;
    class APIWrapper
    {
        // could have multiple references to the same thing?
        // use multiset instead!
        // are references local to transient messages processed in FIFO order?
        // use a queue!  ... etc. etc.
        std::set<MyPtr, SuitableComparator> references_;
    
    public:
        void send(MyPtr const &ref)
        {
            references_.insert(ref);
            system_api_send_obj(ref.get());
        }
        MyPtr receive(void *api)
        {
            MyPtr ref( static_cast<MyClass *>(api)->shared_from_this() );
            references_.erase(ref);
            return ref;
        }
    };
    

    显然(希望)您知道 API 的实际所有权语义,所以以上只是一个广泛的猜测。

    【讨论】:

    • 我也想过这个方案;但是 std::set 也涉及额外的成本,包括时间和空间。
    • 如果我是你,我会更喜欢 std::map,它减少了 shared_from_this 的调用。此外,您仍然必须保护references_不被同时访问。
    • 首先,问题中没有足够的信息来确定您提到的时间和空间开销是否是一个问题 - 您是否有未提及的约束?事实上,使用 map 而不是 set 来保存函数调用是另一种时间/空间权衡,除非你省略了相关的东西,否则这一切都带有过早优化的味道。其次,您明确表示问题与并发无关——您还有什么要补充的吗?
    • 虽然它确实涉及“时间和空间”,但在使用不允许传递智能指针的 API 时,这可能是最简单的解决方案。
    【解决方案4】:

    如何使用以下使用指向智能指针的全局函数。

    template<typename T> void *shared_lock(std::shared_ptr<T> &sp)
    {
        return new std::shared_ptr<T>(sp);
    }
    template<typename T> std::shared_ptr<T> shared_unlock(void *vp)
    {
        std::shared_ptr<T> *psp = static_cast<std::shared_ptr<T,D>*>(sp);
        std::shared_ptr<T> res(*psp);
        delete psp;
        return res;
    }
    

    您有一个额外的新/删除,但原始类型未修改。此外,并发不是问题,因为每次调用shared_lock 都会返回不同的shared_ptr

    为了避免 new/delete 调用,您可以使用对象池,但我认为这不值得。

    更新

    如果你不打算在这件事上使用多线程,那么下面呢?

    template<typename T> struct SharedLocker
    {
        std::vector< std::shared_ptr<T> > m_ptrs;
    
        unsigned lock(std::shared_ptr<T> &sp)
        {
            for (unsigned i = 0; i < m_ptrs.size(); ++i)
            {
                if (!m_ptrs[i])
                {
                     m_ptrs[i] = sp;
                     return i;
                }
            }
            m_ptrs.push_back(sp);
            return m_ptrs.size() - 1;
        }
        std::shared_ptr<T> unlock(unsigned i)
        {
            return std::move(m_ptrs[i]);
        }
    };
    

    我已将void* 更改为unsigned,但这应该不成问题。如果需要,您也可以使用intptr_t

    假设大多数时候向量中只有少数对象,甚至可能不超过 1 个,那么内存分配只会在第一次插入时发生一次。其余时间将是零成本。

    【讨论】:

    • std::shared_ptr&lt;T&gt; *psp = static_cast&lt;std::shared_ptr&lt;T&gt;*&gt;(sp); 可以重写为std::unique_ptr&lt;std::shared_ptr&lt;T&gt;&gt; psp(static_cast&lt;std::shared_ptr&lt;T&gt;*&gt;(sp));,然后就不需要显式的delete(即,您可以将shared_unlock 的实现从4 行减少到2 行)。 :-]
    • 没有shared_ptr&lt;T,D&gt; 这样的东西。 shared_ptr 是著名的only类型模板;删除器和分配器是类型擦除的实现细节。
    • 这就是我所说的“发送一个新的 shared_ptr * 会起作用,但会涉及额外的堆分配。”
    • 我还是更喜欢 enable_lockable_shared_from_this;它几乎可以削减所有成本。
    • @EarthEngine 是的,我在第一次阅读时没有看到您关于堆分配的评论。但是无论如何,如果您打算通过系统调用发送void*,那么额外的新/删除应该是您最不担心的。 “提防过早的优化”。无论如何,您可以查看更新的答案。
    猜你喜欢
    • 1970-01-01
    • 2021-11-18
    • 1970-01-01
    • 1970-01-01
    • 2019-11-24
    • 1970-01-01
    • 2010-12-27
    • 1970-01-01
    • 2020-06-19
    相关资源
    最近更新 更多