Полностью потоковая реализация shared_ptr
Кто-нибудь знает о полностью потокобезопасной реализации shared_ptr
? Например. ускорение реализации shared_ptr
является потокобезопасным для целей (refcounting), а также безопасно для одновременного чтения экземпляров shared_ptr
, но не для записи или для чтения/записи.
(см. Boost docs, примеры 3, 4 и 5).
Существует ли реализация shared_ptr, которая полностью потокобезопасна для экземпляров shared_ptr
?
Странно, что более высокие документы говорят, что:
Объекты shared_ptr обеспечивают тот же уровень безопасности потоков, что и встроенные типы.
Но если вы сравните обычный указатель (встроенный тип) с smart_ptr
, то одновременная запись обычного указателя будет потокобезопасной, но одновременная запись в smart_ptr
не является.
EDIT: я имею в виду реализацию без блокировки в архитектуре x86.
EDIT2: пример использования для такого умного указателя будет там, где есть число рабочих потоков, которые обновляют глобальный shared_ptr с их текущим рабочим элементом и потоком монитора, который принимает произвольные выборки рабочих элементов. Общий-ptr будет владеть рабочим элементом, пока ему не будет назначен другой указатель рабочего элемента (тем самым уничтожая предыдущий рабочий элемент). Монитор получит право собственности на рабочий элемент (тем самым предотвратит уничтожение рабочего элемента), назначив его собственному shared-ptr. Это можно сделать с помощью XCHG и ручного удаления, но было бы неплохо, если бы shared-ptr мог это сделать.
Другим примером является то, что глобальный shared-ptr содержит "процессор" и назначается некоторым потоком и используется каким-то другим потоком. Когда поток "user" видит, что процессор shard-ptr имеет значение NULL, для его обработки используется альтернативная логика. Если это не NULL, это предотвращает уничтожение процессора, назначая его собственному shared-ptr.
Ответы
Ответ 1
Добавление необходимых барьеров для такой полностью потоковой реализации 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 swap, тогда B идет и удаляет его до того, как счетчик ссылок может быть увеличен, будут происходить плохие вещи.
Чтобы справиться с этим, нам понадобится фиктивное значение, которое будет использоваться, когда поток 3 выполняет обновление refcnt:
(примечание: compare_exchange (переменная, ожидаемая, новая) атомарно заменяет значение в переменной новым, если оно в настоящее время равно новому, а затем возвращает true, если оно так успешно)
Тема 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;
Теперь вам нужно добавить цикл с атомикой в середине вашего/чтения/операции. Это не очень хорошо - на некоторых процессорах это может быть очень дорого. Что еще, вы тоже заняты. Вы можете начать увлекаться фьютексами и еще много чего, но к этому моменту вы заново закрепили замок.
Эта стоимость, которая должна покрываться каждой операцией, и очень похожа на природу на то, что блокирует вам в любом случае, является причиной того, что вы, как правило, не видите такие потоковые реализации shared_ptr. Если вам нужна такая вещь, я бы рекомендовал обернуть мьютексы и shared_ptr в класс удобства для автоматизации блокировки.
Ответ 2
Одновременная запись во встроенный указатель, конечно, не является потокобезопасной. Рассмотрите последствия написания с тем же значением в отношении барьеров памяти, если вы действительно хотите свести себя с ума (например, вы могли бы иметь два потока, считая, что один и тот же указатель имел разные значения).
RE: Комментарий - причина, по которой встроенные модули не являются двойными, заключается в том, что они вообще не удаляются (и реализация boost:: shared_ptr, которую я использую, не будет удалять дважды, поскольку она использует специальный атомный приращение и декремент, так что это будет только одно удаление, но тогда результат мог бы иметь указатель от одного и ref count другого. Или почти любая комбинация из двух. Было бы плохо.). Утверждение в документах повышения правильности, так как оно есть, вы получаете те же гарантии, что и со встроенным.
RE: EDIT2 - Первая ситуация, которую вы описываете, очень отличается от использования встроенных и shared_ptrs. В одном (XCHG и ручное удаление) нет счетчика ссылок; вы считаете, что являетесь единственным и единственным владельцем, когда вы это делаете. Если вы используете общие указатели, вы говорите, что другие потоки могут иметь право собственности, что делает вещи намного более сложными. Я считаю, что это возможно с помощью сравнения и замены, но это было бы очень не переносимым.
С++ 0x выходит с помощью библиотеки Atomics, что должно значительно упростить создание общего многопоточного кода. Вероятно, вам придется подождать, пока это не появится, чтобы увидеть хорошие кросс-платформенные эталонные реализации безопасных по потоку интеллектуальных указателей.
Ответ 3
Я не знаю такой реализации интеллектуального указателя, хотя я должен спросить: как это может быть полезно? Единственные сценарии, которые я могу представить, где вы найдете одновременные обновления указателей, - это условия гонки (т.е. Ошибки).
Это не критика - вполне может быть законный прецедент, я просто не могу думать об этом. Пожалуйста, дайте мне знать!
Re: EDIT2
Спасибо, что предоставили пару сценариев. Это звучит так, как будто записи с атомными указателями были бы полезны в этих ситуациях. (Одна мелочь: для второго примера, когда вы написали "Если это не NULL, это предотвращает уничтожение процессора, назначив его собственному shared-ptr", я надеюсь, вы имели в виду, что вы назначаете глобальный общий указатель на локальный общий указатель, а затем проверить, является ли локальный общий указатель NULL - способ, которым вы описали его, подвержен условию гонки, когда глобальный общий указатель становится NULL после того, как вы его протестируете, и перед тем, как назначить его локальному.)
Ответ 4
Вы можете использовать эту реализацию Atomic Reference Counting Pointers, чтобы хотя бы реализовать механизм подсчета ссылок.
Ответ 5
Ваш компилятор уже может предоставить надежные интеллектуальные указатели потоков в новых стандартах С++. Я полагаю, что TBB планирует добавить умный указатель, но я не думаю, что он был включен еще. Тем не менее, вы можете использовать один из TBB потокобезопасных контейнеров.
Ответ 6
Вы можете легко сделать это, включив объект мьютекса с каждым общим указателем и обернув команды инкремента/уменьшения с помощью блокировки.
Ответ 7
Я не думаю, что это так просто, недостаточно обернуть ваши классы sh_ptr с помощью CS. Верно, что если вы поддерживаете одну единственную CS для всех общих указателей, она может обеспечить, чтобы избежать взаимного доступа и удаления объектов sh_ptr среди разных потоков. Но это было бы ужасно, один объект CS для каждого общего указателя был бы настоящим узким местом. Было бы удобно, если бы каждый обертываемый новый ptr -s имел разные CS s ', но таким образом мы должны создать нашу CS динамически и обеспечить копирование классов sh_ptr для передачи этого общего Cs. Теперь мы пришли к той же проблеме: кто гарантирует, что этот Cs ptr уже удален или нет. Мы можем быть немного более умными с изменчивыми m_bReleased флагами на один экземпляр, но таким образом мы не можем закрепить пробелы в безопасности между проверкой флага и использованием общих Cs. Я не вижу абсолютно безопасного решения этой проблемы. Возможно, этот ужасный глобальный Cs был бы незначительным, как убийство приложения. (извините за мой английский)
Ответ 8
Это может быть не совсем то, что вы хотите, но в документации boost::atomic
приведен пример использования атомного счетчика с intrusive_ptr
. intrusive_ptr
является одним из интеллектуальных указателей Boost, он выполняет "интрузивный подсчет ссылок", что означает, что счетчик "встроен" в цель вместо предоставления с помощью интеллектуального указателя.
Boost atomic
Примеры использования:
http://www.boost.org/doc/html/atomic/usage_examples.html
Ответ 9
На мой взгляд, самым простым решением является использование intrusive_ptr
с несколькими незначительными (но необходимыми) модификациями.
Я поделился своей реализацией ниже:
http://www.philten.com/boost-smartptr-mt/