Полностью поточно-ориентированная реализация shared_ptr - PullRequest
13 голосов
/ 14 января 2009

Кто-нибудь знает о полностью поточно-ориентированной shared_ptr реализации? Например. расширенная реализация shared_ptr является поточно-ориентированной для целей (перемонтирование), а также безопасна для одновременного чтения экземпляров shared_ptr, но не для записи или для чтения / записи.

(см. Boost docs , примеры 3, 4 и 5).

Существует ли реализация shared_ptr, которая полностью поточно-ориентирована для shared_ptr экземпляров?

Странно, что буст-документы говорят, что:

Объекты shared_ptr обеспечивают тот же уровень безопасности потоков, что и встроенные типы.

Но если вы сравните обычный указатель (встроенный тип) с smart_ptr, то одновременная запись обычного указателя является поточно-ориентированной, но одновременная запись в smart_ptr не является.

РЕДАКТИРОВАТЬ: я имею в виду реализацию без блокировки на архитектуре x86.

EDIT2: примерный вариант использования для такого умного указателя может быть в том случае, когда имеется ряд рабочих потоков, которые обновляют глобальный shared_ptr своим текущим рабочим элементом и потоком монитора, который берет случайные выборки рабочих элементов. Shared-ptr будет владеть рабочим элементом до тех пор, пока ему не будет назначен другой указатель рабочего элемента (тем самым уничтожив предыдущий рабочий элемент) Монитор получит право собственности на рабочий элемент (тем самым предотвратив уничтожение рабочего элемента), назначив его собственному shared-ptr. Это можно сделать с помощью XCHG и ручного удаления, но было бы неплохо, если бы общий-ptr мог это сделать.

Другой пример: глобальный shared-ptr содержит «процессор», назначается некоторым потоком и используется другим потоком. Когда «пользовательский» поток видит, что процессор shard-ptr равен NULL, он использует некоторую альтернативную логику для выполнения обработки. Если он не равен NULL, он предотвращает разрушение процессора, назначая его собственному shared-ptr.

Ответы [ 9 ]

18 голосов
/ 21 мая 2009

Добавление необходимых барьеров для такой полностью поточно-ориентированной реализации 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: (примечание: сравнение_exchange (переменная, ожидаемое, новое) атомарно заменяет значение в переменной новым, если оно в настоящее время равно новому, и возвращает истину, если это было успешно)

Тема 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 в удобный класс для автоматизации блокировки.

2 голосов
/ 14 января 2009

Одновременная запись во встроенный указатель, безусловно, не является поточно-ориентированной. Подумайте о том, как записать одно и то же значение с учетом барьеров памяти, если вы действительно хотите свести себя с ума (например, у вас может быть два потока, которые думают, что один и тот же указатель имеет разные значения).

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

RE: EDIT2 - первая описанная вами ситуация сильно отличается при использовании встроенных модулей и shared_ptrs. В одном (XCHG и ручное удаление) нет счетчика ссылок; вы предполагаете, что являетесь единственным владельцем, когда вы делаете это. Если вы используете общие указатели, вы говорите, что другие потоки могут владеть, что значительно усложняет задачу. Я считаю, что это возможно при сравнении и обмене, но это было бы очень непереносимо.

C ++ 0x выпускает библиотеку Atomics, которая должна значительно облегчить написание универсального многопоточного кода. Вам, вероятно, придется подождать, пока это не выйдет, чтобы увидеть хорошие кроссплатформенные эталонные реализации интеллектуальных указателей, ориентированных на многопоточность.

1 голос
/ 14 января 2009

Я не знаю такой реализации интеллектуального указателя, хотя я должен спросить: как это поведение может быть полезным? Единственные сценарии, которые я могу придумать, где вы могли бы найти одновременные обновления указателя, - это условия гонки (то есть ошибки).

Это не критика - вполне может быть законный вариант использования, я просто не могу об этом думать. Пожалуйста, дайте мне знать!

Re: EDIT2 Спасибо за предоставление нескольких сценариев. Звучит так, будто записи атомных указателей были бы полезны в таких ситуациях. (Одна небольшая вещь: для второго примера, когда вы написали «Если это не NULL, это предотвращает разрушение процессора путем назначения его собственного shared-ptr», я надеюсь, что вы имели в виду, что вы назначаете глобальный общий указатель на сначала локальный общий указатель , затем проверьте, имеет ли локальный общий указатель значение NULL - как вы описали, он подвержен гонке, когда глобальный общий указатель становится NULL после того, как вы проверите его и перед тем, как назначить местный.)

0 голосов
/ 16 марта 2013

Это может быть не совсем то, что вам нужно, но в документации boost::atomic приведен пример использования атомного счетчика с intrusive_ptr. intrusive_ptr является одним из интеллектуальных указателей Boost, он выполняет «навязчивый подсчет ссылок», что означает, что счетчик «внедряется» в цель вместо предоставления интеллектуальным указателем.

Повышение atomic Примеры использования:

http://www.boost.org/doc/html/atomic/usage_examples.html

0 голосов
/ 17 января 2011

На мой взгляд, самым простым решением является использование intrusive_ptr с несколькими незначительными (но необходимыми) модификациями.

Я поделился своей реализацией ниже:

http://www.philten.com/boost-smartptr-mt/

0 голосов
/ 04 мая 2010

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

0 голосов
/ 11 сентября 2009

Я не думаю, что это так просто, недостаточно просто обернуть ваши классы sh_ptr в CS. Это правда, что если вы поддерживаете одну единственную CS для всех общих указателей, это может гарантировать взаимный доступ и удаление объектов sh_ptr среди разных потоков. Но это было бы ужасно, один объект CS для каждого общего указателя был бы настоящим узким местом. Было бы целесообразно, если бы каждый новый ptr-элемент с возможностью переноса имел разные CS, но таким образом мы должны динамически создавать наши CS и обеспечивать копирование ctors классов sh_ptr для передачи этих общих C. Теперь мы подошли к той же проблеме: кто гарантирует, что этот Cs ptr уже удален или нет. Мы можем быть немного умнее с изменчивыми флагами m_bReleased для каждого экземпляра, но таким образом мы не можем застрять в безопасных промежутках между проверкой флага и использованием общих C. Я не вижу полностью безопасного решения этой проблемы. Может быть, тот ужасный глобальный C был бы второстепенным злом, убившим приложение. (извините за мой английский)

0 голосов
/ 21 мая 2009

Вы можете легко сделать это, включив объект взаимного исключения с каждым общим указателем и добавив команды увеличения / уменьшения к блокировке.

0 голосов
/ 14 января 2009

Ваш компилятор может уже предоставлять поточно-ориентированные интеллектуальные указатели в более новых стандартах C ++. Я считаю, что TBB планирует добавить умный указатель, но я не думаю, что он был включен. Вы можете использовать один из потоковобезопасных контейнеров TBB.

...