Атомарный shared_ptr для незаблокированного односвязного списка

Мне интересно, можно ли создать безблокирующий, потокобезопасный общий указатель для любой из «общих» архитектур, например x64 или ARMv7 / ARMv8.

В своем выступлении о программировании без блокировки на cppcon2014 Херб Саттер представил (частичный) реализация безблокирующего односвязного списка. Реализация выглядит довольно простой, но она основана на атомарной реализации shared_ptr, которой еще нет в стандартной библиотеке, или на использовании специализированных std::atomic... функций. Это особенно важно, поскольку одиночные вызовы push / pop потенциально вызывают несколько атомарных загрузок / хранилищ и compare_exchange операций.

Проблема, которую я вижу (и я думаю, что некоторые вопросы в докладе были в том же направлении), заключается в том, что для того, чтобы это была фактическая структура данных без блокировок, эти атомарные операции сами должны быть свободными от блокировок. Я не знаю какой-либо реализации стандартной библиотеки для std::atomic... функций без блокировки и - по крайней мере, с помощью короткого поиска в Google / SO - я также не нашел предложения, как реализовать безблокировочную специализацию для std::atomic<std::shared_ptr> .

Теперь, прежде чем я буду тратить на это время, я хотел спросить:

  • Знаете ли вы, можно ли вообще написать безблокирующий атомарный общий указатель?
  • Существуют ли уже какие-либо реализации, которые я упустил из виду и - в идеале - даже совместимы с тем, что вы ожидаете от std::atomic<std::shared_ptr>? Для упомянутой очереди особенно потребуется CAS-операция.
  • Если нет возможности реализовать это на текущих архитектурах, видите ли вы какие-либо другие преимущества в реализации Herb по сравнению с «обычным» связанным списком, защищенным блокировкой?

Для справки, вот код от Herb Sutter (может содержать мои опечатки):

template<class T> 
class slist {
    struct Node { T t;  std::shared_ptr<Node> next; };
    std::atomic<std::shared_ptr<Node>> head;        
public:
    class reference{
        std::shared_ptr<Node> p;
    public:
        reference(std::shared_ptr<Node> p_){}
        T& operator*(){ return p->t; }
        T* operator->(){ return &p->t; }
    };
    auto find(T t) const {
        auto p = head.load();
        while (p && p-> != t) {
            p = p - next;
        }
        return reference(move(p));
    }
    void push_front(T t) {
        auto p = std::make_shared<Node>();
        p->t = t;
        p->next = head;
        while (!head.compare_exchange_weak(p->next, p)) {}
    }
    void pop_front() {
        auto p = head.load();
        while (p && !head.compare_exchange_weak(p, p - next)) { ; }
    }
};

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


Чтобы объяснить мою мотивацию:
Это в основном академический вопрос. Я не собираюсь внедрять свой собственный список свободных блокировок в производственный код, но я считаю эту тему интересной, и на первый взгляд презентация Херба показалась мне хорошим введением. Однако, размышляя над этим вопросом и комментарием @sehe к моему ответу, я вспомнил этот разговор, у меня был еще один посмотрите на это и поймете, что не имеет большого смысла называть реализацию Herb без блокировки, если для ее примитивных операций требуются блокировки (что они сейчас и делают). Поэтому мне было интересно, является ли это просто ограничением текущих реализаций или фундаментальным недостатком в дизайне.


person MikeMB    schedule 07.07.2015    source источник
comment
Известны ли вам предложения по атомарным интеллектуальным указателям??   -  person dyp    schedule 07.07.2015
comment
@dyp: Мне известны предложения вплоть до N4162, но в нем не упоминается версия без блокировок. Напротив: это говорит о приросте производительности, потому что атомарный флаг, используемый для реализации спин-блокировки, может быть сохранен как часть интеллектуального указателя.   -  person MikeMB    schedule 07.07.2015
comment
Некоторые точки данных: И libstdc ++, и libc ++ используют глобальный массив мьютексов для защиты атомарного доступа к shared_ptrs через функции [util.smartptr.shared.atomic]. Они хэшируют адрес объекта shared_ptr и используют глобальную хеш-таблицу мьютексов фиксированного размера.   -  person dyp    schedule 07.07.2015
comment
Вы читали предложения по текущим атомарным общим ptr-операциям? N2674 упоминает, что они можно сделать без блокировки ..   -  person dyp    schedule 07.07.2015
comment
@dyp: Нет, пока нет. Спасибо за ссылку, посмотрю.   -  person MikeMB    schedule 08.07.2015
comment
Как я помню, для атомарного shared_ptr требуется аппаратная поддержка инструкций CAS2, которые обрабатывают атомарные операции для двух независимых областей памяти. то есть вам нужно протестировать и установить внутренний указатель и refCount атомарно.   -  person DU Jiaen    schedule 09.11.2015


Ответы (3)


Я добавляю это как ответ, так как он слишком длинный, чтобы уместиться в комментарии:

Что нужно учитывать. Свободный от блокировок shared_ptr не необходим для реализации структур данных без блокировок / без ожидания.

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

Конечно, наличие lock-free atomic_shared_ptr как части Стандарта было бы огромным подспорьем. Но это не единственный способ освободить память для структур данных без блокировок, это наивная реализация поддержки списка узлов, подлежащих удалению в неактивных точках выполнения (работает только в сценариях с низким уровнем конкуренции), указатели опасностей, ролл- собственный атомарный подсчет ссылок с использованием разделенного счетчика.

Что касается производительности, @mksteve верен, код без блокировок не гарантированно превосходит альтернативы на основе блокировок, если, возможно, он не работает в высокопараллельной системе, предлагающей настоящий параллелизм. Его цель - обеспечить максимальный параллелизм, и из-за этого мы обычно получаем потоки, выполняющие меньше ожидания за счет выполнения большего объема работы.

PS Если вас это интересует, вам стоит взглянуть на C ++ Concurrency in Action Энтони Уильямса. Целая глава посвящена написанию кода без блокировок / без ожидания, который предлагает хорошую отправную точку, проходя через реализации безблокировочного стека и очереди.

person Alla    schedule 27.10.2015

  • # P1 #
  • # P2 #

Я думаю, что std :: atomic _... предлагает форму реализации , где slist будет выполнять специальные atomic_ запросы к shared_ptr. Проблема с разделением этого на два класса (std :: atomic и std :: shared_ptr) заключается в том, что каждый из них имеет ограничения, которых необходимо придерживаться, чтобы функционировать. Разделение классов делает невозможным знание общих констант.

В slist, который знает оба элемента, он может помочь в ситуации, и поэтому, вероятно, будут работать атомарные _... функции.

  • # P5 #

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

Это не дает гарантии лучшей производительности, чем заблокированная реализация, но дает гарантию того, что взаимоблокировки не возникнут.

Представьте, что T требуется блокировка для выполнения копирования, она также могла принадлежать некоторым операциям вне списка. Тогда была бы возможна тупиковая ситуация, если бы она принадлежала, и была вызвана реализация slist на основе блокировки.

Я думаю, что CAS реализован в std::compare_exchange_weak, поэтому он не зависит от реализации.

Текущие алгоритмы без блокировки для сложных структур (например, вектор, карта), как правило, значительно менее эффективны, чем алгоритмы блокировки, Dr Dobbs: структуры данных без блокировок, но предлагаемое преимущество (улучшенная производительность потоков) может значительно улучшить производительность компьютеров, которые, как правило, имеют большое количество простаивающих процессоров.

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

person mksteve    schedule 31.08.2015

Можно написать незаблокируемый разделяемый ptr, поскольку единственное, что нужно изменить, - это счетчик. Сам ptr только копируется, поэтому здесь не требуется особого внимания. При удалении это должен быть последний экземпляр, чтобы не было других копий в других потоках, поэтому никто не будет увеличивать в одно и то же время.
Но, сказав это, std :: atomic> wuold будет очень специализированной вещью, поскольку это не совсем примитивный тип.

Я видел несколько реализаций списков без блокировки, но ни в одной из них не было общих указателей. Эти контейнеры обычно имеют специальное назначение, и поэтому существует соглашение об их использовании (когда / кто создает / удаляет), поэтому использование общих указателей не требуется.
Кроме того, общие указатели создают накладные расходы, которые противоречат нашим целям с низкой задержкой это в первую очередь привело нас к безблокировочному домену.

Итак, возвращаясь к вашему вопросу - я думаю, что это возможно, но я не понимаю, зачем это нужно.
Если вам действительно нужно что-то подобное, лучше подойдет переменная-член refCount. < br>

Я не вижу особых преимуществ в конкретной реализации Херба, может быть, кроме академической, но списки без блокировки имеют очевидную мотивацию - отсутствие блокировки. Они часто служат очередями или просто разделяют набор узлов между потоками, страдающими аллергией на блокировки.
Может быть, нам стоит спросить Херба .. Херб? ты слушаешь?

РЕДАКТИРОВАТЬ: Следуя всем приведенным ниже комментариям, я реализовал безблокирующий односвязный список. Список довольно сложен, чтобы предотвратить удаление общих ptrs во время доступа к ним. Он слишком велик для публикации здесь, но вот основные идеи:
- Основная идея состоит в том, чтобы спрятать удаленные записи в отдельном месте - сборщике мусора - чтобы сделать их недоступными для последующих действий.
- Атомарная ссылка count увеличивается при входе в каждую функцию (push_front, pop_front и front) и автоматически уменьшается при выходе. При уменьшении до нуля счетчик версий увеличивается. Все в одной атомарной инструкции.
- Когда общий ptrs необходимо стереть, в pop_front он помещается в сборщик мусора. Для каждого номера версии есть сборщик мусора. Сборщик мусора реализован с использованием более простого списка без блокировок, который может использовать только push_front или pop_all. Я создал кольцевой буфер из 256 GC, но можно применить и другую схему.
- GC версии сбрасывается при увеличении версии, а затем общие ptrs удаляют держатели.
Итак, если вы вызываете pop_front, без все остальное работает, счетчик ссылок увеличивается до 1, передний общий ptr помещается в GC [0], счетчик ссылок возвращается в ноль, а версия до 1, GC [0] сбрасывается - он уменьшает общий ptr, который мы вытащили, и, возможно, удаляет принадлежащий ему объект.

Теперь запишем файл shared_ptr без блокировок. Я считаю, что это выполнимо. Вот идеи, о которых я подумал:
- Вы можете иметь своего рода спин-блокировку, используя младшие биты указателя на держатель, так что вы можете разыменовать его только после того, как заблокировали его. Вы можете использовать другой бит для inc / dec и т.п. как связанный список.
- У вас может быть центральный реестр общих указателей. Это не связано с описанной выше проблемой, но будет сложно масштабировать без периодических всплесков задержки.

Подводя итог, в настоящее время я считаю, что вся эта идея является спорной. Если вы найдете какой-то другой подход, который не страдает большими проблемами - мне будет очень любопытно узнать об этом :)
Спасибо!

person BitWhistler    schedule 22.07.2015
comment
Спасибо, но вам также понадобится операция атомарного сравнения и замены. И даже копирование shared_ptr атомарно для меня звучит нетривиально, потому что вам нужно увеличить счетчик ссылок и скопировать адрес. Или мне не хватает какой-то очевидной техники для этого? - person MikeMB; 22.07.2015
comment
CAS необходим для списка, но не в общем ptr. Свободный от блокировок shared_ptr будет содержать указатель на struct Holder {T * ptr; int count; }; Итак, Держатель - это то, что вы копируете, и в этом нет необходимости в атомарности. Только счет inc / decment является атомарным. - person BitWhistler; 22.07.2015
comment
Вам все равно нужно разыменовать указатель на держатель и увеличить счетчик ссылок. В противном случае держатель ссылки может быть уничтожен сразу после разыменования указателя на него. Вы описываете текущую реализацию std :: share_ptr (за исключением отсутствующего счетчика слабых ссылок), которая не является потокобезопасной. - person MikeMB; 22.07.2015
comment
Описанный вами сценарий не существует. Удаление происходит, когда это последняя копия, и эта последняя копия может существовать только в локальной области, поэтому никакой другой поток не будет копировать ее одновременно. Это соглашение о разделяемом ptr - его не следует явно разрушать в совместно используемом контейнере. Удаление в общем контейнере происходит только при уничтожении контейнера, которое само по себе должно происходить от одного владельца. - person BitWhistler; 22.07.2015
comment
На самом деле это сложнее, чем я думал изначально. Представьте, что один поток удаляется из общего контейнера, в то время как (= сразу после) другой поток его скопировал. Счетчик должен увеличиваться при возврате из контейнера, прежде чем он уменьшится при удалении. Никакая CAS в этом не поможет. Дай подумать, может, напишу код, а я отредактирую ответ. - person BitWhistler; 22.07.2015
comment
Я бы рекомендовал а) посмотреть выступление травы и б) прочитать этот вопрос. Может быть, это проясняет мою проблему. Я посмотрю, смогу ли я включить в свой вопрос дополнительную информацию. - person MikeMB; 22.07.2015
comment
@BitWhistler Разместите свой код. Я уверен, что люди захотят его увидеть! - person CinchBlue; 24.07.2015