Кто-нибудь знает о полностью потокобезопасной реализации shared_ptr? Например. Boost реализация shared_ptr является потокобезопасной для целей (счетчик ссылок), а также безопасна для одновременного чтения экземпляра shared_ptr, но не для записи или для чтения / записи.
(см. Документы Boost, примеры 3, 4 и 5).
Есть ли реализация shared_ptr, полностью ориентированная на многопотоковое исполнение для экземпляров shared_ptr?
Странно, что документы по ускорению говорят, что:
shared_ptr objects offer the same level of thread safety as built-in types.
Но если вы сравните обычный указатель (встроенный тип) с smart_ptr, то одновременная запись обычного указателя будет потокобезопасной, а одновременная запись в smart_ptr - нет.
Обновлено: Я имею в виду реализацию без блокировки на архитектуре x86.
EDIT2: пример использования такого интеллектуального указателя будет, когда есть несколько рабочих потоков, которые обновляют глобальный shared_ptr своим текущим рабочим элементом и потоком монитора, который берет случайные выборки рабочих элементов. Shared-ptr будет владеть рабочим элементом до тех пор, пока ему не будет назначен другой указатель рабочего элемента (тем самым уничтожив предыдущий рабочий элемент). Монитор получит право владения рабочим элементом (тем самым предотвращая его уничтожение), назначив его своему собственному shared-ptr. Это можно сделать с помощью XCHG и ручного удаления, но было бы неплохо, если бы это мог сделать shared-ptr.
Другой пример: глобальный shared-ptr содержит «процессор», назначается некоторым потоком и используется другим потоком. Когда «пользовательский» поток видит, что процессор shard-ptr имеет значение NULL, он использует некоторую альтернативную логику для выполнения обработки. Если он не NULL, он предотвращает разрушение процессора, назначая его своему собственному shared-ptr.
По крайней мере, на x86, если указатель выровнен правильно, операция записи является атомарной.
А как насчет одновременной записи в одном потоке и удаления в другом? (удаление - это, по сути, особый вид записи; тот, который стирает указанный элемент).
Макс: то, что вы описываете, - это одновременное чтение и запись. delete не изменяет значение самой переменной-указателя, поэтому он не считается записью - это значение указал на, которое (потенциально) записывается (деструктором, если он существует).
j_random_hacker: Хорошее замечание.
"операция записи атомарна" Это не делает его "потокобезопасным" в общем смысле.





Одновременная запись во встроенный указатель определенно небезопасна для потоков. Если вы действительно хотите свести с ума (например, у вас могут быть два потока, которые думают, что один и тот же указатель имеет разные значения), подумайте о последствиях записи в одно и то же значение в отношении барьеров памяти.
RE: Комментарий - причина, по которой встроенные модули не удаляются дважды, заключается в том, что они вообще не удаляются (а реализация boost :: shared_ptr, которую я использую, не будет выполнять двойное удаление, поскольку в ней используются специальные атомарные приращение и декремент, так что это будет только одно удаление, но тогда в результате может быть указатель от одного и счетчик ссылок от другого. Или почти любая их комбинация. Это было бы плохо.). Заявление в документации по ускорению и так верное, вы получаете те же гарантии, что и со встроенным.
RE: EDIT2 - Первая ситуация, которую вы описываете, сильно отличается между использованием встроенных модулей и shared_ptrs. В одном (XCHG и ручное удаление) счетчика ссылок нет; вы предполагаете, что вы единственный владелец, когда делаете это. Если вы используете общие указатели, вы говорите, что другие потоки могут владеть, что значительно усложняет задачу. Я считаю, что это возможно с помощью сравнения и замены, но это было бы очень непереносимым.
C++ 0x выходит с библиотекой Atomics, которая должна значительно упростить написание универсального многопоточного кода. Вероятно, вам придется подождать, пока это не выйдет, чтобы увидеть хорошие кроссплатформенные эталонные реализации поточно-безопасных интеллектуальных указателей.
Я согласен, что два процессора могут видеть разное значение встроенного указателя (т.е. не видеть обновлений, выполненных другим процессором), но это может быть приемлемо для приложения. Я предполагаю, что это не потокобезопасность в строгом смысле слова, но, по крайней мере, не произойдет сбой (boost shared_ptr удалит дважды).
Я не знаю такой реализации интеллектуального указателя, но должен спросить: чем такое поведение может быть полезно? Единственные сценарии, в которых я могу найти одновременные обновления указателей, - это состояния гонки (то есть ошибки).
Это не критика - вполне может быть законный вариант использования, я просто не могу об этом подумать. Пожалуйста, дайте мне знать!
Re: EDIT2 Спасибо, что предоставили пару сценариев. Похоже, что в таких ситуациях была бы полезна запись атомарного указателя. (Одна маленькая вещь: во втором примере, когда вы написали «Если он не NULL, он предотвращает уничтожение процессора, назначая его собственному общему ptr», я надеюсь, вы имели в виду, что назначаете глобальный общий указатель на сначала локальный общий указатель тогда проверяет, является ли локальный общий указатель NULL - как вы описали, он подвержен состоянию гонки, когда глобальный общий указатель становится NULL после того, как вы проверите его, и до того, как вы назначите его локальному.)
«Надеюсь, вы имели в виду, что сначала назначаете глобальный общий указатель на локальный общий указатель» - Да, мне следовало использовать лучшую формулировку
Ваш компилятор может уже предоставлять потокобезопасные интеллектуальные указатели в новых стандартах C++. Я считаю, что TBB планирует добавить интеллектуальный указатель, но я не думаю, что он еще включен. Однако вы можете использовать один из поточно-ориентированных контейнеров TBB.
Добавление необходимых барьеров для такой полностью потокобезопасной реализации shared_ptr, вероятно, повлияет на производительность. Рассмотрим следующую расу (примечание: псевдокод изобилует):
Поток 1: global_ptr = A;
Поток 2: global_ptr = B;
Поток 3: local_ptr = global_ptr;
Если мы разделим это на составляющие операции:
Поток 1:
A.refcnt++;
tmp_ptr = exchange(global_ptr, A);
if (!--tmp_ptr.refcnt) delete tmp_ptr;
Поток 2:
B.refcnt++;
tmp_ptr = exchange(global_ptr, B);
if (!--tmp_ptr.refcnt) delete tmp_ptr;
Поток 3:
local_ptr = global_ptr;
local_ptr.refcnt++;
Ясно, что если поток 3 считывает указатель после свопа A, тогда B удаляет его до того, как счетчик ссылок может быть увеличен, могут произойти неприятности.
Чтобы справиться с этим, нам нужно использовать фиктивное значение, пока поток 3 выполняет обновление refcnt: (примечание: compare_exchange (переменная, ожидаемое, новое) атомарно заменяет значение в переменной новым, если оно в настоящее время равно new, затем возвращает истину, если это было сделано успешно)
Поток 1:
A.refcnt++;
tmp_ptr = global_ptr;
while (tmp_ptr == BAD_PTR || !compare_exchange(global_ptr, tmp_ptr, A))
tmp_ptr = global_ptr;
if (!--tmp_ptr.refcnt) delete tmp_ptr;
Поток 2:
B.refcnt++;
while (tmp_ptr == BAD_PTR || !compare_exchange(global_ptr, tmp_ptr, A))
tmp_ptr = global_ptr;
if (!--tmp_ptr.refcnt) delete tmp_ptr;
Поток 3:
tmp_ptr = global_ptr;
while (tmp_ptr == BAD_PTR || !compare_exchange(global_ptr, tmp_ptr, BAD_PTR))
tmp_ptr = global_ptr;
local_ptr = tmp_ptr;
local_ptr.refcnt++;
global_ptr = tmp_ptr;
Теперь вам нужно добавить цикл с атомами в середине вашей операции / read /. Это нехорошо - для некоторых процессоров это может быть очень дорого. Более того, вы тоже заняты-ждете. Вы можете начать изобретать фьютексы и тому подобное, но к этому моменту вы заново изобрели замок.
Эта стоимость, которую приходится нести каждой операции и которая очень похожа по своей природе на то, что вам может дать блокировка, является причиной того, почему вы обычно не видите таких поточно-безопасных реализаций shared_ptr. Если вам это нужно, я бы порекомендовал обернуть мьютекс и shared_ptr в удобный класс для автоматизации блокировки.
Очень хороший ответ, спасибо. Это то, что я искал, то есть каковы последствия этого. Я думаю, ваш пример работал бы без B?
На самом деле, он все равно прервался бы только с потоками 1 и 3 - добавление потока 2 было просто, чтобы показать, что мы уже инициализировали его чем-то первым.
«добавление барьеров повлияет на производительность» !! Что ж, тогда вы должны знать, что прямо сейчас существуют препятствия как в boost, так и в std :: shared (класс base_ref_cnt). Поскольку они используют атомики, а атомики действительно выполняют необходимый сброс загрузки / сохранения на процессоре (забор памяти). внутренняя поддержка компилятором и атомарным <> C++ 11 даже хуже, чем забор процессора, он также является забором компилятора (без переупорядочения загрузки / хранения, переданного атомарному). shared_ptr медленные, сегодня уже. И что еще хуже, они не являются потокобезопасными. Они безопасны только в том случае, если у вас нет слабых ссылок.
@curiousguy, потому что вы не можете обновить 2 счетчика атомарно без использования мьютекса. И они используют только атомарные свопы / inc / dec, поэтому я чувствую, что должна быть возможность нарушить внутренние инварианты. Я реализовал собственный shared_ptr 2 раза и не смог решить эту проблему. Может быть, разработчики std и гении, мне нужно будет когда-нибудь проверить, как они это делают.
@ v.oddou Можете привести конкретный пример небезопасного кода?
@curiousguy Ваш запрос заставил меня провести небольшое исследование. Я обнаружил, что в Boost sp_counted_base_gcc_x86.hpp их уловка заключается в разделении двух отсчетов: int weak_count_; // #weak + (#shared != 0) Ключ в этом искусственном +1. Таким образом, удаление счетчика ссылок не зависит от состояния гонки, с которым я столкнулся в своих реализациях. Это гениально :) Это не ответ на ваш вопрос. Но в своем исследовании я также нашел прекрасную небольшую статью justsoftwaresolutions.co.uk/threading/…. Конкретные примеры небезопасного кода: проблема ABA, условия гонки
Вы можете легко сделать это, включив объект мьютекса в каждый общий указатель и заключив команды увеличения / уменьшения в блокировку.
Я не думаю, что это так просто, недостаточно обернуть ваши классы sh_ptr CS. Это правда, что если вы поддерживаете одну единую CS для всех общих указателей, это может гарантировать избежание взаимного доступа и удаления объектов sh_ptr между разными потоками. Но это было бы ужасно, ведь один объект CS для каждого общего указателя был бы настоящим узким местом. Было бы удобно, если бы каждый оборачиваемый новый ptr -s имел разные CS, но таким образом мы должны создать нашу CS динамически и обеспечить копирование ctors классов sh_ptr для передачи этих общих Cs. Теперь мы подошли к той же проблеме: кто гарантирует, что этот Cs ptr уже удален или нет. Мы можем быть немного умнее с изменчивыми флагами m_bReleased для каждого экземпляра, но таким образом мы не сможем устранить пробелы в безопасности между проверкой флага и использованием общих Cs. Я не вижу полностью безопасного решения этой проблемы. Может быть, этот ужасный глобальный C будет второстепенным недостатком, убившим приложение. (Извините за мой английский)
Вы можете использовать эту реализацию Указатели подсчета атомных ссылок, по крайней мере, для реализации механизма подсчета ссылок.
На мой взгляд, самое простое решение - использовать intrusive_ptr с небольшими (но необходимыми) модификациями.
Я поделился своей реализацией ниже:
http://www.philten.com/boost-smartptr-mt/
К сожалению, ваш код также не является потокобезопасным. В intrusive_ptr_release() поток может быть вытеснен после оценки состояния if, но до delete. Затем другой поток может, например, вызвать intrusive_ptr_add_ref() и intrusive_ptr_release() и удалить ptr перед исходным потоком, который затем продолжает работу, как будто ничего не произошло, и пытается удалить ptr очередной раз.
Возможно, это не совсем то, что вам нужно, но документация boost::atomic предоставляет пример того, как использовать атомарный счетчик с intrusive_ptr. intrusive_ptr - один из интеллектуальных указателей Boost, он выполняет «навязчивый подсчет ссылок», что означает, что счетчик «встроен» в цель, а не предоставляется интеллектуальным указателем.
Примеры использования Boost atomic:
"одновременная запись обычного указателя потокобезопасна" - вы в этом уверены?