Мой вопрос: в какой степени RAII заменяет другие шаблоны проектирования
как сборщик мусора? Я предполагаю, что ручное управление памятью
не используется для представления совместного владения в системе.
Я не уверен, что назвал бы это шаблоном проектирования, но, по моему столь же твердому мнению, и просто говоря о ресурсах памяти, RAII решает почти все проблемы, которые GC может решить, вводя меньше.
Является ли совместное владение объектами признаком плохого дизайна?
Я разделяю мысль о том, что совместное владение далеко, далеко от идеала в большинстве случаев, потому что дизайн высокого уровня не обязательно требует этого. Единственный раз, когда я обнаружил, что это неизбежно, - это внедрение постоянных структур данных, когда оно, по крайней мере, усвоено как деталь реализации.
Самая большая проблема, которую я нахожу с GC или просто общим владением, заключается в том, что это не освобождает разработчика от ответственности, когда дело доходит до ресурсов приложения, но может создать иллюзию этого. Если у нас есть такой случай (Scene
является единственным логическим владельцем ресурса, но другие вещи содержат ссылку / указатель на него, как камера, хранящая список исключений сцены, определенный пользователем, чтобы исключить из рендеринга):
И скажем, ресурс приложения подобен изображению, а его время жизни привязано к пользовательскому вводу (например, изображение должно быть освобождено, когда пользователь запрашивает закрыть документ, содержащий его), затем работа по правильному освобождению ресурса то же самое с GC или без него.
Без GC мы могли бы удалить его из списка сцен и позволить вызывать его деструктор, одновременно вызывая событие, позволяющее Thing1
, Thing2
и Thing3
установить для них указатели на ноль или удалить их из списка, чтобы у них не было висящих указателей.
С GC это в основном то же самое. Мы удаляем ресурс из списка сцен, одновременно вызывая событие, позволяющее Thing1
, Thing2
и Thing3
установить их ссылки на null или удалить их из списка, чтобы сборщик мусора мог его собрать.
Ошибка молчаливого программиста, которая летит под радаром
Разница в этом сценарии заключается в том, что происходит, когда возникает ошибка программиста, например, Thing2
не обрабатывает событие удаления. Если Thing2
хранит указатель, у него теперь есть висячий указатель, и у нас может произойти сбой. Это катастрофично, но что-то, что мы могли бы легко уловить в наших модульных и интеграционных тестах, или, по крайней мере, что-то, что тестировщики или тестировщики поймут довольно быстро. Я не работаю в критическом или критическом контексте безопасности, поэтому, если аварийный код каким-то образом удалось доставить, все равно не так уж и плохо, если мы сможем получить сообщение об ошибке, воспроизвести его, обнаружить его и довольно быстро его исправить .
Если Thing2
хранит сильную ссылку и разделяет владение, у нас есть очень тихая логическая утечка, и изображение не будет освобождено до тех пор, пока Thing2
не будет уничтожено (что может быть не уничтожено до выключения). В моем домене эта тихая природа ошибки очень проблематична, так как она может остаться незамеченной даже после отправки, пока пользователи не начнут замечать, что работа в приложении в течение часа приводит к тому, что она занимает, например, гигабайты памяти, и начинает замедляться пока они не перезапустят его. И в этот момент мы могли накопить большое количество этих проблем, поскольку им так легко летать под радаром, как ошибка истребителя-невидимки, и нет ничего, что мне не нравилось бы больше, чем ошибки истребителей-невидимок.
И именно из-за этой безмолвной природы я склонен не любить совместную собственность со страстью, и TBH я никогда не понимал, почему GC так популярен (может быть, это моя особая область - я, по общему признанию, очень не знаю тех, которые являются миссией -критически, например) до такой степени, что я жажду новых языков без GC . Я обнаружил, что расследование всех таких утечек, связанных с совместным владением, было очень трудоемким, а иногда расследование в течение нескольких часов, только чтобы обнаружить, что утечка была вызвана исходным кодом вне нашего контроля (сторонний плагин).
Слабые ссылки
Слабые ссылки концептуально идеальны для Thing1
, Thing2
и Thing3
. Это позволило бы им определить, когда ресурс был задним числом уничтожен задним числом, не продлевая его срок службы, и, возможно, мы могли бы гарантировать сбой в этих случаях, или некоторые могли бы даже изящно справиться с этим задним числом. Для меня проблема в том, что слабые ссылки могут быть преобразованы в сильные ссылки, и наоборот, поэтому среди внутренних и сторонних разработчиков кто-то может все же небрежно хранить сильную ссылку в Thing2
, даже если бы слабая ссылка была бы гораздо более уместно.
В прошлом я пытался как можно больше поощрять использование слабых ссылок внутренней командой и документировать, что они должны использоваться в SDK. К сожалению, было трудно продвигать эту практику среди такой широкой и смешанной группы людей, и мы все же получили свою долю логических утечек.
Легкость, с которой любой в любой момент времени может продлить срок жизни объекта намного дольше, чем это необходимо, просто сохраняя сильную ссылку на него в своем объекте, начинает становиться очень пугающей перспективой, если смотреть на огромную кодовую базу это утечка огромных ресурсов. Я часто хотел бы, чтобы для хранения любых сильных ссылок в качестве члена какого-либо объекта требовался очень четкий синтаксис, который, по крайней мере, заставил бы разработчика дважды подумать о том, чтобы делать это без необходимости.
Явное уничтожение
Поэтому я склоняюсь к явному уничтожению постоянных ресурсов приложения, например:
on_removal_event:
// This is ideal to me, not trying to release a bunch of strong
// references and hoping things get implicitly destroyed.
destroy(app_resource);
... так как мы можем рассчитывать на освобождение ресурса. Мы не можем быть полностью уверены, что что-то в системе не будет иметь висячий указатель или слабую ссылку, но, по крайней мере, эти проблемы легко обнаружить и воспроизвести в тестировании. Они не остаются незамеченными целую вечность и накапливаются.
Один хитрый случай всегда был для меня многопоточным. В этих случаях вместо полноценной сборки мусора или, скажем, shared_ptr
я считаю полезным просто как-то отложить уничтожение:
on_removal_event:
// *May* be deferred until threads are finished processing the resource.
destroy(app_resource);
В некоторых системах, где постоянные потоки объединены таким образом, что у них есть событие processing
, например, мы можем пометить ресурс, который должен быть уничтожен, отложенным способом во временном интервале, когда потоки не обрабатываются ( почти начинает ощущаться как GC "стоп-мир", но мы ведем явное уничтожение). В других случаях мы могли бы использовать, скажем, подсчет ссылок, но таким образом, чтобы избежать shared_ptr
, где счетчик ссылок ресурса начинается с нуля и будет уничтожен с использованием вышеуказанного явного синтаксиса, если только поток локально не продлит свое время жизни путем увеличения счетчика временно (например: использование ресурса с областью действия в функции локального потока).
Каким бы обходным это ни казалось, оно избегает показа ссылок GC или shared_ptr
внешнему миру, что может легко соблазнить некоторых разработчиков (внутри вашей команды или стороннего разработчика) хранить сильные ссылки (например, shared_ptr
) в качестве члена объекта, подобного Thing2
, и, таким образом, непреднамеренно продлевает срок службы ресурса, и, возможно, намного, намного дольше, чем нужно (возможно, вплоть до закрытия приложения).
RAII
Между тем RAII автоматически устраняет физические утечки так же, как и GC, но, кроме того, он работает не только с памятью, но и с другими ресурсами. Мы можем использовать его для мьютекса с областью действия, файла, который автоматически закрывается при уничтожении, мы можем использовать его даже для автоматического устранения внешних побочных эффектов с помощью ограждения области видимости и т. Д. И т. Д.
Так что, если бы у меня был выбор, и я должен был выбрать один, это легко RAII для меня. Я работаю в домене, где эти тихие утечки памяти, вызванные общим владением, являются абсолютно убийственными, и сбой висячих указателей на самом деле предпочтителен, если (и он, вероятно, будет) обнаружен на ранних этапах тестирования. Даже в каком-то действительно неясном событии, которое обнаруживается поздно, если оно проявляется в сбое рядом с местом, где произошла ошибка, это все же предпочтительнее, чем использование инструментов профилирования памяти и попытка выяснить, кто забыл выпустить ссылку, пробираясь через миллионы строк кода. По моему очень прямому мнению, GC создает больше проблем, чем решает для моего конкретного домена (VFX, что несколько похоже на игры с точки зрения организации сцены и состояния приложения), и одна из причин, помимо этих очень тихих утечек совместного владения, заключается в том, что это может создать у разработчиков ложное впечатление, что им не нужно думать об управлении ресурсами и владении постоянными ресурсами приложений, в то же время непреднамеренно вызывая логические утечки влево и вправо.
«Когда RAII терпит неудачу»
Единственный случай, с которым я когда-либо сталкивался за всю свою карьеру, когда я не мог придумать какой-либо возможный способ избежать совместного владения каким-либо видом, - это когда я реализовал библиотеку постоянных структур данных, например:
Я использовал его для реализации неизменяемой структуры данных сетки, в которой можно изменять части, не делая их уникальными, например (тест с 4 миллионами четырехугольников):
В каждом отдельном кадре создается новая сетка, когда пользователь перетаскивает ее и лепит. Разница в том, что новая сетка имеет сильные ссылки на части, которые не сделаны уникальными с помощью кисти, так что нам не нужно копировать все вершины, все полигоны, все ребра и т. Д. Неизменяемая версия упрощает безопасность потоков, исключений, неразрушающее редактирование, отмена систем, создание экземпляров и т. д.
В этом случае вся концепция неизменной структуры данных вращается вокруг совместного владения, чтобы избежать дублирования данных, которые не были сделаны уникальными. Это подлинный случай, когда мы не можем избежать совместного владения, несмотря ни на что (по крайней мере, я не могу придумать какой-либо возможный путь).
Это единственный случай, когда нам может понадобиться GC или подсчет ссылок, с которым я столкнулся. Другие, возможно, сталкивались с некоторыми из них, но, по моему опыту, очень и очень немногие случаи действительно нуждаются в совместном владении на уровне проектирования.