Блокировка shared_ptr - PullRequest
       17

Блокировка shared_ptr

11 голосов
/ 10 ноября 2011

У меня есть общий объект, который необходимо отправить системному API и извлечь его позже.Системный API получает только void *.Я не могу использовать shared_ptr :: get (), потому что он не увеличивает счетчик ссылок и может быть освобожден другими потоками перед извлечением из системного API.Отправка нового shared_ptr * будет работать, но требует дополнительного выделения кучи.

Один из способов сделать это - позволить объекту, полученному из enable_shared_from_this.Однако, поскольку этот шаблон класса владеет только слабым_птром, этого недостаточно для того, чтобы не допустить освобождения объекта.

Поэтому мое решение выглядит следующим образом:

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());

Есть ли более простой способсделать это?

Недостаток этого решения:

  • для него требуется дополнительно shared_ptr<MyClass> в макете объекта MyClass, а также weak_ptr в базовом классе enable_shared_from_this.

  • Как я уже упоминал в комментариях, доступ к lock() и unlock() одновременно НЕ является безопасным.

  • Хуже всего то, что это решение может поддерживать lock() только один раз перед вызовом unlock().Если один и тот же объект должен использоваться для нескольких системных вызовов 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) против O (n) (пробел; время равно O (log (n))) или аналогично, но абсолютно больше, чем O(1)) игра.

Ответы [ 4 ]

3 голосов
/ 11 ноября 2011

Настроив предыдущий ответ, я наконец-то получил следующее решение:

//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 голосов
/ 10 ноября 2011

Теперь у меня есть идея следующего:

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`.

2 голосов
/ 10 ноября 2011

Почему бы просто не обернуть API void * во что-то, отслеживающее время жизни ссылок этого 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, так что вышеизложенное является всего лишь догадкой.

1 голос
/ 10 ноября 2011

Как насчет использования следующих глобальных функций, которые используют указатели на умные указатели.

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, вы можете использовать пул объектов, но я думаю, что это не стоит усилий.

UPDATE :

Если вы не собираетесь использовать несколько потоков в этом вопросе, как насчет следующего?

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, выделение памяти произойдет только один раз при первой вставке. А в остальное время будет стоить ноль.

...