четверг, 20 ноября 2014 г.

Как устроен weak_ptr

Для чего нужен.

В предыдущем посте я рассказывал про устройство shared_ptr. К сожалению, он подвержен классической проблеме циклических ссылок. Для решения этой проблемы используется weak_ptr.

Как решить проблему циклических ссылок.

В объект счётчика ссылок добавляется счётчик слабых ссылок. weak_ptr увеличивает этот счётчик при создании и копировании. При этом объект счётчика ссылок не удаляется до тех пор, пока есть хотя бы один shared_ptr или один weak_ptr, т.к. weak_ptr необходимо обращаться к счётчику ссылок даже если нет ни одного живого shared_ptr. Но сам объект, на который указывает shared_ptr, может быть удалён, если больше не осталось живых shared_ptr.
Код объекта счётчика ссылок теперь будет выглядеть так:
template <typename T>
class sp_counted {
public:
    explicit sp_counted(T *p) noexcept
        : shared_count(1),
          weak_count(1),
          ptr(p) {}

    void add_ref() noexcept {
        ++shared_count;
    }

    void release() noexcept {
        if (!--shared_count) {
            // Если последний shared_ptr удалился, удаляем объект
            delete ptr;
            if (!--weak_count) {
                // Если нет слабых ссылок, удаляем объект счётчика
                destroy();
            }
        }
    }

    void add_ref_weak() noexcept {
        ++weak_count;
    }

    void release_weak() noexcept {
        if (!--weak_count) {
            // Если последний weak_ptr удалился, удаляем объект счётчика.
            // Т.к. shared_ptr тоже увеличивает weak_count при создании,
            // нет необходимости проверять значение shared_count
            destroy();
        }
    }

    size_t use_count() const noexcept {
        return shared_count.load();
    }

    // Попытка увеличить счётчик shared_count из weak_ptr
    // Потокобезопасен, lock-free
    void add_ref_lock() {
        // Сохраняем текущее значение shared_count
        size_t cur_value(shared_count.load());
        do {
            // Если счётчик сильных ссылок равен нулю (т.е. нет больше живых shared_ptr),
            // то новый shared_ptr создавать не из чего.
            if (!cur_value) {
                throw std::bad_weak_ptr();
            }
          // Пытаемся увеличить счётчик shared_count на единицу
          // Если в промежутке между сохранением shared_count в cur_value, shared_count изменился,
          // то операция compare_exchange_weak вернёт false, запишет новое значение shared_count в cur_value,
          // и цикл повторится
        } while (shared_count.compare_exchange_weak(cur_value, cur_value + 1));
    }

private:
    void destroy() noexcept {
        delete this;
    }

private:
    // Счётчик ссылок shared_ptr
    std::atomic<size_t> shared_count;
    // Счётчик ссылок weak_ptr
    std::atomic<size_t> weak_count;
    T *ptr;
};
Вопрос: почему вызов add_ref_lock безопасен? Ведь в другом потоке shared_ptr в деструкторе может удалить объект sp_counted, и обращение к переменной shared_count будет некорректным.
Ответ: потому что деструктор shared_ptr не удалит объект sp_counted, т.к. есть живая слабая ссылка (weak_count != 0).
Код weak_ptr будет выглядеть так:

template <typename T>
class weak_ptr {
    friend class shared_ptr<T>;
public:
    weak_ptr() noexcept : ptr(nullptr), counted(nullptr) {}

    weak_ptr(const weak_ptr &other) noexcept : ptr(other.ptr), counted(other.counted) {
        add_ref_weak();
    }

    weak_ptr(const shared_ptr<T> &p) noexcept : ptr(p.ptr), counted(p.counted) {
        add_ref_weak();
    }

    weak_ptr &operator=(const weak_ptr &other) noexcept {
        release_weak();

        ptr = other.ptr;
        counted = other.counted;

        add_ref_weak();

        return *this;
    }

    weak_ptr &operator=(const shared_ptr<T> &p) {
        release_weak();

        ptr = p.ptr;
        counted = p.counted;

        add_ref_weak();

        return *this;
    }

    // Пытаемся сделать shared_ptr. Для этого вызывается конструктор shared_ptr(const weak_ptr &amp;);
    // В случае невозможности создать shared_ptr возвращается пустой объект
    shared_ptr<T> lock() noexcept {
        try {
            return shared_ptr<T>(*this);
        } catch (const std::bad_weak_ptr &) {
            return shared_ptr<T>();
        }
    }

    size_t use_count() const noexcept {
        return counted != nullptr ? counted->use_count() : 0;
    }

private:
    void add_ref_weak() noexcept {
        if (counted) {
            counted->add_ref_weak();
        }
    }

    void release_weak() noexcept {
        if (counted) {
            counted->release_weak();
        }
    }

private:
    T *ptr;
    sp_counted<T> *counted;
};
В код shared_ptr добавляется конструктор shared_ptr(const weak_ptr &p):
template <typename T>
class weak_ptr;

template <typename T>
class shared_ptr {
    friend class weak_ptr<T>;
public:
    shared_ptr() noexcept : ptr(nullptr), counted(nullptr) {}

    // excaption safe конструктор
    explicit shared_ptr(T *p) {
        std::unique_ptr<T> holder(p);
        // new может кинуть исключение, и если p не передать в unique_ptr,
        // память под p потеряется
        counted = new sp_counted<T>(holder.get());
        ptr = holder.release();
    }

    ~shared_ptr() noexcept {
        release();
    }

    shared_ptr(const shared_ptr<T> &other) noexcept : ptr(other.ptr), counted(other.counted) {
        add_ref();
    }

    shared_ptr(const weak_ptr<T> &p) : ptr(p.ptr), counted(p.counted) {
        if (counted) {
            // Пытаемся увеличить счётчик ссылок объекта
            // В случае неудачи сгенерируется исключение std::bad_weak_ptr()
            counted->add_ref_lock();
        } else {
            throw std::bad_weak_ptr();
        }
    }

    shared_ptr &operator=(const shared_ptr<T> &other) noexcept {
        // Освобождаем владение предыдущим указателем
        release();

        // Выполняем присваивание
        ptr = other.ptr;
        counted = other.counted;

        // Устанавливаем владение новым указателем
        add_ref();

        // Ура! Я не забыл вернуть *this!
        return *this;
    }

    T *get() const noexcept {
        return ptr;
    }

    size_t use_count() const noexcept {
        return counted != nullptr ? counted->use_count() : 0;
    }

private:
    void add_ref() noexcept {
        if (counted) {
            counted->add_ref();
        }
    }

    void release() noexcept {
        if (counted) {
            counted->release();
        }
    }

private:
    T *ptr;
    sp_counted<T> *counted;
};

А что с потокобезопасностью?

Гарантии потокобезопасности такие же как и у shared_ptr. Но на всякий случай повторю: любая операция с weak_ptr потокобезопасна, если каждый поток хранит копию weak_ptr. При этом каждая копия может хранить сслыку на один shared_ptr. Хранить ссылки на один weak_ptr в разных потоках непотокобезопасно.

Комментариев нет:

Отправить комментарий