Полностью потокобезопасная реализация shared_ptr

Кто-нибудь знает о полностью потокобезопасной реализации 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.

"одновременная запись обычного указателя потокобезопасна" - вы в этом уверены?

OJ. 14.01.2009 06:52

По крайней мере, на x86, если указатель выровнен правильно, операция записи является атомарной.

Magnus Hiie 14.01.2009 07:01

А как насчет одновременной записи в одном потоке и удаления в другом? (удаление - это, по сути, особый вид записи; тот, который стирает указанный элемент).

Max Lybbert 14.01.2009 11:05

Макс: то, что вы описываете, - это одновременное чтение и запись. delete не изменяет значение самой переменной-указателя, поэтому он не считается записью - это значение указал на, которое (потенциально) записывается (деструктором, если он существует).

j_random_hacker 15.01.2009 14:45

j_random_hacker: Хорошее замечание.

Max Lybbert 15.01.2009 21:26

"операция записи атомарна" Это не делает его "потокобезопасным" в общем смысле.

curiousguy 08.10.2011 18:25
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
13
6
17 747
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

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

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

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

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

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

Magnus Hiie 14.01.2009 07:11

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

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

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

«Надеюсь, вы имели в виду, что сначала назначаете глобальный общий указатель на локальный общий указатель» - Да, мне следовало использовать лучшую формулировку

Magnus Hiie 21.01.2009 14:23

Ваш компилятор может уже предоставлять потокобезопасные интеллектуальные указатели в новых стандартах 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?

Magnus Hiie 01.10.2009 17:32

На самом деле, он все равно прервался бы только с потоками 1 и 3 - добавление потока 2 было просто, чтобы показать, что мы уже инициализировали его чем-то первым.

bdonlan 01.10.2009 19:58

«добавление барьеров повлияет на производительность» !! Что ж, тогда вы должны знать, что прямо сейчас существуют препятствия как в boost, так и в std :: shared (класс base_ref_cnt). Поскольку они используют атомики, а атомики действительно выполняют необходимый сброс загрузки / сохранения на процессоре (забор памяти). внутренняя поддержка компилятором и атомарным <> C++ 11 даже хуже, чем забор процессора, он также является забором компилятора (без переупорядочения загрузки / хранения, переданного атомарному). shared_ptr медленные, сегодня уже. И что еще хуже, они не являются потокобезопасными. Они безопасны только в том случае, если у вас нет слабых ссылок.

v.oddou 05.12.2013 04:53

@curiousguy, потому что вы не можете обновить 2 счетчика атомарно без использования мьютекса. И они используют только атомарные свопы / inc / dec, поэтому я чувствую, что должна быть возможность нарушить внутренние инварианты. Я реализовал собственный shared_ptr 2 раза и не смог решить эту проблему. Может быть, разработчики std и гении, мне нужно будет когда-нибудь проверить, как они это делают.

v.oddou 13.06.2018 05:25

@ v.oddou Можете привести конкретный пример небезопасного кода?

curiousguy 13.06.2018 06:11

@curiousguy Ваш запрос заставил меня провести небольшое исследование. Я обнаружил, что в Boost sp_counted_base_gcc_x86.hpp их уловка заключается в разделении двух отсчетов: int weak_count_; // #weak + (#shared != 0) Ключ в этом искусственном +1. Таким образом, удаление счетчика ссылок не зависит от состояния гонки, с которым я столкнулся в своих реализациях. Это гениально :) Это не ответ на ваш вопрос. Но в своем исследовании я также нашел прекрасную небольшую статью justsoftwaresolutions.co.uk/threading/…. Конкретные примеры небезопасного кода: проблема ABA, условия гонки

v.oddou 13.06.2018 13:01

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

Я не думаю, что это так просто, недостаточно обернуть ваши классы 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 очередной раз.

Andreas Magnusson 24.10.2012 16:12

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

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

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

Другие вопросы по теме