Подтвердить что ты не робот

В чем разница между std:: shared_ptr и std:: experimental:: atomic_shared_ptr?

Я прочитал следующую статью Антони Уильямса, и, как я понял в дополнение к общему количеству атомов в std::shared_ptr в std::experimental::atomic_shared_ptr, фактический указатель на общий объект также является атомарным?

Но когда я прочитал ссылку на подсчитанную версию lock_free_stack, описанную в книге Антония о С++ Concurrency, мне кажется, что те же самые апплики также для std::shared_ptr, потому что функции, подобные std::atomic_load, std::atomic_compare_exchnage_weak, применяются к экземплярам std::shared_ptr.

template <class T>
class lock_free_stack
{
public:
  void push(const T& data)
  {
    const std::shared_ptr<node> new_node = std::make_shared<node>(data);
    new_node->next = std::atomic_load(&head_);
    while (!std::atomic_compare_exchange_weak(&head_, &new_node->next, new_node));
  }

  std::shared_ptr<T> pop()
  {
    std::shared_ptr<node> old_head = std::atomic_load(&head_);
    while(old_head &&
          !std::atomic_compare_exchange_weak(&head_, &old_head, old_head->next));
    return old_head ? old_head->data : std::shared_ptr<T>();
  }

private:
  struct node
  {
    std::shared_ptr<T> data;
    std::shared_ptr<node> next;

    node(const T& data_) : data(std::make_shared<T>(data_)) {}
  };

private:
  std::shared_ptr<node> head_;
};

Какова точная разница между этими двумя типами интеллектуальных указателей, и если указатель в экземпляре std::shared_ptr не является атомарным, то почему это возможно в случае реализации стека блокировки без блокировки?

4b9b3361

Ответ 1

Атомная "вещь" в shared_ptr - это не общий указатель, а контрольный блок, на который он указывает. что до тех пор, пока вы не будете мутировать shared_ptr через несколько потоков, вы в порядке. обратите внимание, что копирование shared_ptr только мутирует блок управления, а не сам shared_ptr.

std::shared_ptr<int> ptr = std::make_shared<int>(4);
for (auto i =0;i<10;i++){
   std::thread([ptr]{ auto copy = ptr; }).detach(); //ok, only mutates the control block 
}

Мутирование самого общего указателя, такого как присвоение ему разных значений из нескольких потоков, является расходом данных, например:

std::shared_ptr<int> ptr = std::make_shared<int>(4);
std::thread threadA([&ptr]{
   ptr = std::make_shared<int>(10);
});
std::thread threadB([&ptr]{
   ptr = std::make_shared<int>(20);
});    

Здесь мы мутируем блок управления (это нормально), а также сам общий указатель, указывая на разные значения из нескольких потоков. Это не нормально.

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

Другим решением является использование стандартных функций, которые вы указали, например std::atomic_compare_exchange_weak. Это делает работу по синхронизации общих указателей ручной, что нам не нравится.

Здесь используется атомный общий указатель. Вы можете мутировать общий указатель из нескольких потоков, не опасаясь гонки данных и без использования каких-либо блокировок. Отдельные функции будут членами, и их использование будет намного более естественным для пользователя. Этот вид указателя чрезвычайно полезен для незакрепленных структур данных.

Ответ 2

Вызов std::atomic_load() или std::atomic_compare_exchange_weak() на shared_ptr функционально эквивалентен вызову atomic_shared_ptr::load() или atomic_shared_ptr::atomic_compare_exchange_weak(). Между ними не должно быть разницы в производительности. Вызов std::atomic_load() или std::atomic_compare_exchange_weak() на atomic_shared_ptr будет синтаксически избыточным и может или не может нести штраф за производительность.

Ответ 3

N4162 (pdf), предложение для атомных интеллектуальных указателей, имеет хорошее объяснение. Вот цитата из соответствующей части:

Согласованность. Насколько я знаю, [util.smartptr.shared.atomic] функции являются единственными атомными операциями в стандарте, которые недоступны с помощью типа atomic. И для всех типов кроме shared_ptr, мы учим программистов использовать атомные типы в С++, а не atomic_* C-стиле. И это частично из-за...

Корректность. Использование бесплатных функций делает код уязвимым к ошибкам и по умолчанию. Намного лучше писать atomic один раз на само объявление переменной и знать все обращения будет атомарным, вместо того, чтобы забыть использовать atomic_*операции при каждом использовании объекта, даже, по-видимому, простой. Последний стиль подвержен ошибкам; например, "делать это неправильно" означает просто писать пробелы (например, head вместо atomic_load(&head)), так что в этом стиле каждое использование переменной "неправильно по умолчанию". Если вы забудете напишите вызов atomic_* в одном месте, ваш код будет по-прежнему успешно скомпилировать без каких-либо ошибок или предупреждений, он появится работать ", в том числе, вероятно, пройти большинство испытаний, но все равно будет содержать тихая гонка с поведением undefined, которое обычно возникает как прерывистый труднопроизносимые сбои, часто/обычно в поле, и я ожидаю также в некоторых случаях уязвимости, которые можно использовать. Эти классы ошибок устраняются простым объявлением переменной atomic, потому что тогда его сейф по умолчанию и написать тот же набор ошибки требуют явного не-пробельного кода (иногда явного memory_order_* и обычно reinterpret_cast ing).

Производительность. atomic_shared_ptr<> как отдельный тип имеет важное преимущество в эффективности перед функции в [util.smartptr.shared.atomic] - он может просто хранить дополнительный atomic_flag (или аналогичный) для внутренней спин-блокировки как обычно для atomic<bigstruct>. Напротив, существующие автономные функции должны использоваться для любых произвольных shared_ptrобъекта, хотя подавляющее большинство shared_ptr будет никогда не используется атомарно. Это делает свободные функции неотъемлемо менее эффективны; например, реализация может потребовать каждый shared_ptr для переноса служебных данных внутренней спин-блокировки переменная (лучше concurrency, но значительные накладные расходы за shared_ptr), иначе библиотека должна поддерживать данные вида структуры для хранения дополнительной информации для shared_ptr, которые фактически используется атомарно, или (худшее и, по-видимому, общее в практика) библиотека должна использовать глобальную спин-блокировку.

Ответ 4

atomic_shared_ptr - это уточнение API. shared_ptr уже поддерживает атомные операции, но только при использовании соответствующих атомных функций, не являющихся членами.. Это подвержено ошибкам, так как неатомные операции остаются доступными и слишком легки для непреднамеренного программиста. atomic_shared_ptr менее подвержен ошибкам, поскольку он не выдает никаких неатомных операций.

shared_ptr и atomic_shared_ptr выставлять различные API-интерфейсы, но они необязательно должны выполняться по-разному; shared_ptr уже поддерживает все операции, отображаемые atomic_shared_ptr. Сказав это, атомные операции shared_ptr не так эффективны, как могли бы быть, потому что они также должны поддерживать неатомные операции. Поэтому есть причины, по которым atomic_shared_ptr может быть реализовано по-разному. Это связано с принципом единой ответственности. "Сущность с несколькими разрозненными целями... часто предлагает искаженные интерфейсы для любых своих конкретных целей, потому что частичное перекрытие между различными областями функциональности стирает видение, необходимое для четкого воплощения каждого из них". (Sutter and Alexandrescu 2005, стандарты кодирования С++)