Может ли быть гоночная ситуация в следующем коде? - PullRequest
2 голосов
/ 17 июня 2019

Мы знаем, что приведенный ниже код деструктора должен освободить блок управления, если это последний smart_ptr, указывающий на управляемый ресурс. Возможно ли, что у нас есть гоночная проблема между «если» и «удалить» ниже? Что делать, если мы попытаемся создать совершенно новый объект smart_ptr в другом потоке прямо справа ПОСЛЕ «if» и ДО «delete»?

// Thread D:
// smart_ptr destructor
~smart_ptr() {
  if (control_block_ptr->refs.fetch_sub(1, memory_order_acq_rel) == 0) {
    delete control_block_ptr;
  }
}

Ответы [ 4 ]

2 голосов
/ 18 июня 2019

Сначала исправление:

control_block_ptr->refs.fetch_sub(1, memory_order_acq_rel) == 0

Вы имеете в виду == 1.fetch_sub возвращает значение до вычитания, а не после.

После выяснения:

Возможно ли, что у нас возникла гоночная проблема между«если» и «удалить» ниже?Что, если мы попытаемся создать совершенно новый объект smart_ptr в другом потоке справа сразу после «if» и ДО «delete»?

Итак, вы уничтожаете объект smart_ptr.Если control_block_ptr->refs равно 1, то это означает, что текущий smart_ptr является единственным объектом , которому принадлежит блок управления, верно?Так что же вас беспокоит?

В конце концов, некоторые "совершенно новые smart_ptr" будут иметь свои собственные "совершенно новые" control_block_ptr и собственный счетчик ссылок.Это никоим образом не помешает уничтожаемому.

Единственная причина, по которой может возникнуть проблема, - это если есть гонка между уничтожением smart_ptr и его копированием.И под «этим» я подразумеваю буквально тот же объект ;не просто smart_ptr, но тот же уникальный владелец этого управляющего блока, который уничтожается.

Посмотрите, если у вас было два smart_ptr объекта, разделяющих одно и то же состояние, копировать один, уничтожая другой, вполне нормально.потому что независимо от порядка этих операций все работает.Один из них уменьшит счетчик ссылок;другой будет увеличивать его.Но поскольку счетчик ссылок начинается с 2, счетчик ссылок никогда не достигает 0.

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

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

1 голос
/ 18 июня 2019

Проще говоря, нет.

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

Вот что считается конечной точкой гонки, является ли ресурс освобожден, потому что пересчет(RC) достигает нуля;вопрос «какая точная операция, выполняемая каким потоком, обнуляет RC», является интересной конечной точкой: неявное предположение об использовании RC для управления ресурсами в многопоточном контексте заключается в том, что любой поток (у которого есть последний владелец) может освободитьресурс.

По определению RC - это сумма отдельных строго положительных вкладов каждого владельца в RC (это 1, так как RC - это количество владельцев, но это не очень важно).В абстрактной настройке RC также может быть формализован как набор владельцев, и целое число будет эффективным представлением необходимой информации из-за особенностей RC:

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

Таким образом, вы можете представить число как список владельцев, каждый из которых представлен вертикальной линией, как когда детизапоминайте числа (3 = |||), и только отдельный владелец знает свой собственный бар (вы можете сказать, что все столбцы одинаковые или имеют разные цвета).(Очевидно, что целые числа физически представлены в двоичном виде.)

В установках, где существуют только владельцы (никакая операция не может ссылаться на RC через «слабую ссылку»), есть только две основные операции над множеством владельцев:

  • дублировать владельца: сделать нового владельца из владельца
  • удалить владельца

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

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

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

То, что есть условие гонкина множестве подразумевается, что должна использоваться внутренняя атомика (или подобная альтернатива), но владельцам должно быть все равно.Было бы катастрофично, если бы в представлении был случайно удален определенный «бар», это означало бы, что владелец не учитывается, и это был бы владелец зомби: он бы поверил, что он владеет ресурсом, но на самом деле он ничего не будет иметь,Атомарная операция RMW (чтение, запись, изменение) гарантирует невозможность выполнения операции: любой атомарный объект, модифицированный исключительно с помощью операции RMW, не может потерять модификации .

Сама концепция владения подразумевает, что она не может быть создана из ничего: вы можете стать только владельцем чего-то:

  • путем создания этой вещи
  • или приобретения права собственности (у кого-то) у кого-то, у кого уже есть

Это просто здравый смысл.

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

Эти свойства делают реализацию истинного владения RC очень простой.Это отличается, когда слабые ссылки, то есть RC-only watcher , вводят изображение: инструмент измерения RC обеспечивает гарантию того, что он может производить измерения на RC в любой момент в будущем, независимо от того, будет ли онресурс управляемого пользователя все еще имеет владельца (владельцев), независимо от того, существует ли ресурс пользователя.При слабых ссылках RC может быть прочитан как ноль атомарной операцией, которая не является вертикальной чертой в графическом представлении набора владельцев.Это означает, что время жизни RC становится отличным от времени жизни пользовательского ресурса: сам RC становится другим управляемым ресурсом (внутренним RC).

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

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

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

Таким образом, слабые ссылки могут быть полезны для моделирования проблем реального мира, когда наблюдатель не может принудительно сохранить ресурс, если внешний фактор разрушит его, ноМожно наблюдать за эволюцией ресурса, пока он существует.Продвижение слабой ссылки на сильную делает снимок живости ресурса в момент, когда преобразование выполнено, и по своей природе является грубым.

Обратите внимание, что любое значимое использование многих примитивов является грубым внекоторый уровень : вы используете мьютекс, потому что вы не знаете, какому потоку сначала потребуется эксклюзивный доступ к ресурсу;если бы вы знали точный порядок выполнения, вы бы сериализовали потоки и вообще избежали сложности потоков.Гонка за получением доступа к ресурсу не является ошибкой.Только когда правильность выполнения программы зависит от определенного порядка событий, возникает ошибка.

Мы знаем, что приведенный ниже код деструктора должен освободить блок управления, если это последний указатель smart_ptrк управляемому ресурсу.

Да, и это правильно, желаемое поведение, когда срок полезного использования RC заканчивается, когда он достигает нуля, то есть когда реализуется истинное владение и слабые ссылки не поддерживаются.Это не будет правильным для Boost или стандарта shared_ptr, который поддерживает «слабые» указатели, такие как наблюдатели, не являющиеся владельцами.

Хотя условие гонки, которое вы упомянули в семантически невозможном, есть проблемыздесь:

~smart_ptr() {
  if (control_block_ptr->refs.fetch_sub(1, memory_order_acq_rel) == 0) {
    delete control_block_ptr;
  }
}

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

Где сам пользовательский ресурс управляется? Это в деструкторе объекта *control_block_ptr? Можете ли вы опубликовать немного больше кода, чтобы получить полную картину?

Также вы использовали операцию post -decrement fetch_sub вместо операции перед декрементом: операции post возвращают предыдущее значение , затем выполняют операцию. С помощью операции post особое интересное значение RC, на которое вы хотите воздействовать, значение до того, как последний владелец перестанет быть владельцем, равно 1, а не 0 .

1 голос
/ 18 июня 2019

Как правило (по крайней мере для интеллектуальных указателей надстроек и стандартных библиотек) объекты интеллектуальных указателей сами по себе не предназначены для обеспечения безопасности потоков. Только управление / время жизни объекта, на который они указывают, является потокобезопасным. Сам объект smart_ptr небезопасен для одновременного использования в нескольких потоках, но безопасно иметь несколько разных объектов smart_ptr, ссылающихся на одни и те же базовые данные, которые используются одновременно.

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

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

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

0 голосов
/ 18 июня 2019

Если конструктор не глючит, то он увидит, что refcount уже равен нулю, и, следовательно, процесс уничтожения начался и необратим.Таким образом, из POV других потоков объект уже уничтожается, когда счетчик обращений достигает нуля, даже если delete на самом деле еще не освободил память.

Плюс, если деструктор приводит счетчик к нулю,это означает, что больше не shared_pointer объектов для копирования-конструирования.(Если только у вас нет ошибки «использовать после освобождения» для самого объекта, а не для блока управления. В этом случае вы используете его неправильно: обычно вы не делаете ссылки на shared_ptr объекты.).

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


Но в общем случае для объектов с пересчетом, если увидеть refcount = 0, значит уничтожение прошло точкувозврата нет , и ваша попытка получить новую ссылку на него не удалась.(т. е. произошло после разрушения в глобальном порядке, установленном атомным счетчиком).

Это "видение", конечно, будет возвращать значение fetch_add(+1).


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

...