Будут ли барьеры полной памяти вокруг std :: shared_ptr use_count () сделать его надежным счетчиком? - PullRequest
1 голос
/ 27 мая 2019

Я реализую потокобезопасный «ленивый синхронизированный» набор в виде связанного списка узлов, связанных с shared_ptr.Алгоритм из "Искусство многопроцессорного программирования".Я добавляю функцию is_empty(), которая должна быть линеаризуемой с существующими функциями: contains(), add(), remove().В приведенном ниже коде вы можете увидеть remove двухэтапный процесс.Сначала он «ленивый» помечает узел, устанавливая marked = nullptr, затем он физически перемещает связанный список next указатели.

Измененные классы для поддержки is_empty ()

template <class T>
class LazySet : public Set<T> {
    public:
      LazySet ();
      bool contains (const T&) const;
      bool is_empty ()         const;
      bool add      (const T&);
      bool remove   (const T&);
    private:
      bool validate(const std::shared_ptr<Node>&, const std::shared_ptr<Node>&);
      class Node;
      std::shared_ptr<Node> head;
      std::shared_ptr<bool> counter; //note: type is unimportant, will never change true/fase
};

template <class T>
class LazySet<T>::Node {
    public:
      Node ();
      Node (const T&);
      T key;
      std::shared_ptr<bool> marked; //assume initialized to = LazySet.counter
                                    // nullptr means it's marked; otherwise unmarked
      std::shared_ptr<Node> next;
      std::mutex mtx;
};

Соответствующие измененныеметоды для поддержки is_empty

template <class T>
bool LazySet<T>::remove(const T& k) {
    std::shared_ptr<Node> pred;
    std::shared_ptr<Node> curr;
    while (true) {
        pred = head;
        curr = atomic_load(&(head->next));
        //Find window where key should be in sorted list
        while ((curr) && (curr->key < k)) {
            pred = atomic_load(&curr);
            curr = atomic_load(&(curr->next));
        }
        //Aquire locks on the window, left to right locking prevents deadlock
        (pred->mtx).lock();
        if (curr) { //only lock if not nullptr
            (curr->mtx).lock();
        }
        //Ensure window didn't change before locking, and then remove
        if (validate(pred, curr)) {
            if (!curr) { //key doesn't exist, do nothing
                //## unimportant ##
            } else { //key exists, remove it
                atomic_store(&(curr->marked), nullptr); //logical "lazy" remove
                atomic_store(&(pred->next), curr->next) //physically remove
                (curr->mtx).unlock();
                (pred->mtx).unlock();
                return true;
            }
        } else {
            //## unlock and loop again ##
        }
    }
}

template <class T>
bool LazySet<T>::contains(const T& k) const {
    std::shared_ptr<Node> curr;
    curr = atomic_load(&(head->next));
    //Find window where key should be in sorted list
    while ((curr) && (curr->key < k)) {
        curr = atomic_load(&(curr->next));
    }
    //Check if key exists in window
    if (curr) {
        if (curr->key == k) { //key exists, unless marked
            return (atomic_load(&(curr->marked)) != nullptr);
        } else { //doesn't exist
            return false;
        }
    } else { //doesn't exist
        return false;
    }
}

Node.marked изначально были простым булом, а LazySet.counter не существовало.Выбор сделать их shared_ptr должен был иметь возможность атомарно изменять как счетчик количества узлов, так и ленивую метку удаления на узлах.Модификация обоих одновременно в remove() необходима для того, чтобы is_empty() был линеаризуемым с contains().(Это не может быть отдельная метка bool и счетчик int без CAS двойной ширины или чего-то в этом роде.) Я надеюсь реализовать счетчик с помощью функции use_count() shared_ptr, но в многопоточных контекстах это только приблизительное значение из-за relaxed_memory_order.

Я знаю, что автономные заборы обычно являются плохой практикой, и я не слишком знаком с их использованием. Но если бы я реализовал is_empty, как показано ниже, ограждения гарантировали бы, что это уже не приближение, а точное значение для надежного счетчика?

template <class T>
bool LazySet<T>::is_empty() const {
    // ## SOME FULL MEMORY BARRIER
    if (counter.use_count() == 1) {
        // ## SOME FULL MEMORY BARRIER
        return true
    }
    // ## SOME FULL MEMORY BARRIER
    return false
}

Я спрашиваю только потому, что LWG Issue 2776 гласит:

Мы не можем сделать use_count() надежным, не добавив значительно больше ограждений.

Ответы [ 2 ]

1 голос
/ 27 мая 2019

Расслабленный порядок памяти здесь не проблема.use_count не является «надежным», потому что к тому времени, когда значение будет возвращено, оно может изменить .При получении самого значения нет гонки данных, но ничто не мешает изменить это значение перед любым условным оператором, основанным на этом значении.

Таким образом, вы не можете ничего с ним сделать, поскольку оно все еще полагается на его значениебыть осмысленным (за исключением того, что если вы все еще держите экземпляр shared_ptr, то счетчик использования не будет равен 0).Единственный способ сделать его надежным - это предотвратить его изменение.Таким образом, вам нужно иметь мьютекс.

И этот мьютекс должен блокироваться не только вокруг вызова и использования use_count, но также каждый раз, когда вы раздаете один из этих shared_ptr, которыевы получаете use_count от.

0 голосов
/ 29 мая 2019
// ## SOME FULL MEMORY BARRIER
if (counter.use_count() == 1) {
    // ## SOME FULL MEMORY BARRIER

С ранее установленным ограждением вы можете быть уверены, что можете «видеть» результаты всех сбросов (в том числе во время назначения и уничтожения) всех владельцев в других потоках.Ограничение получения дает всем последующим расслабленным операциям семантику получения, предотвращая их от «извлечения значений в будущем» (что в любом случае является семантическим безумием и, вероятно, делает все программы формально UB).

(Нет значимого ограничениячто вы можете поставить после звонка.)

...