stringtranslate.com

Управление ресурсами (вычисления)

В компьютерном программировании под управлением ресурсами понимаются методы управления ресурсами (компонентами с ограниченной доступностью).

Компьютерные программы могут управлять своими собственными ресурсами [ какими? ] с использованием функций, предоставляемых языками программирования (Elder, Jackson & Liblit (2008) — это обзорная статья, в которой сравниваются различные подходы), или может выбрать управление ими с помощью хоста — операционной системы или виртуальной машины — или другой программы.

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

Контроль доступа

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

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

Утечка ресурсов

Формально управление ресурсами (предотвращение утечек ресурсов) состоит в обеспечении освобождения ресурса тогда и только тогда, когда он успешно получен. Эту общую проблему можно абстрагировать как код « до, тело и после », которые обычно выполняются в этом порядке, с условием, что код после вызывается тогда и только тогда, когда код перед успешно завершается, независимо от того, код тела выполняется успешно или нет. Это также известно как выполнение вокруг [1] или сэндвич кода и происходит в различных других контекстах, [2] таких как временное изменение состояния программы или отслеживание входа и выхода в подпрограмму . Однако управление ресурсами является наиболее часто упоминаемым приложением. В аспектно-ориентированном программировании такое выполнение логики является формой совета .

В терминологии анализа потока управления высвобождение ресурсов должно постдоминировать над успешным приобретением ресурсов; [3] неспособность убедиться, что это ошибка, и путь кода, нарушающий это условие, приводит к утечке ресурсов. Утечки ресурсов часто представляют собой незначительные проблемы, обычно не приводящие к сбою программы, а вызывающие некоторое замедление работы программы или всей системы. [2] Однако они могут вызывать сбои – как самой программы, так и других программ – из-за истощения ресурсов: если в системе заканчиваются ресурсы, запросы на получение не выполняются. Это может представлять собой ошибку безопасности , если атака может привести к истощению ресурсов. Утечки ресурсов могут произойти при обычном выполнении программы (например, если просто забыть освободить ресурс) или только в исключительных обстоятельствах, например, когда ресурс не освобождается, если в другой части программы возникает исключение. Утечки ресурсов очень часто возникают из-за раннего выхода из подпрограммы либо из-за returnоператора, либо из-за исключения, вызванного самой подпрограммой или более глубокой подпрограммой, которую она вызывает. Хотя высвобождение ресурсов из-за операторов возврата может быть обработано путем осторожного освобождения внутри подпрограммы перед возвратом, исключения не могут быть обработаны без каких-либо дополнительных языковых средств, которые гарантируют выполнение кода освобождения.

Более тонко: успешное получение ресурса должно доминировать над его освобождением, иначе код попытается освободить ресурс, который он не захватил. Последствия такого неправильного выпуска варьируются от молчаливого игнорирования до сбоя программы или непредсказуемого поведения. Эти ошибки обычно проявляются редко, поскольку для первого сбоя требуется выделение ресурсов, что обычно является исключительным случаем. Кроме того, последствия могут быть несерьезными, поскольку программа уже может давать сбой из-за невозможности получить необходимый ресурс. Однако это может помешать восстановлению после сбоя или превратить штатное завершение работы в неупорядоченное завершение работы. Это условие обычно обеспечивается путем первой проверки того, что ресурс был успешно получен перед его освобождением, либо путем наличия логической переменной для записи «успешно получено» - которой не хватает атомарности, если ресурс получен, но переменная флага не может быть обновлена, или наоборот. – или дескриптором ресурса, который имеет тип, допускающий значение NULL , где «null» означает «не удалось получено», что обеспечивает атомарность.

Конфликт за ресурсы

Управление памятью

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

Лексическое управление и явное управление

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

Основные техники

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

f  =  открыть ( имя файла ) ... f . закрывать ()

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

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

Утечку ресурсов можно решить в языках, поддерживающих finallyконструкцию (например, Python), поместив тело в tryпредложение, а релиз — в finallyпредложение:

f  =  open ( имя файла ) попробуйте :  ... наконец :  f . закрывать ()

Это гарантирует правильный выпуск, даже если в теле есть возврат или выдано исключение. Кроме того, обратите внимание, что получение происходит перед предложением try, гарантируя, что finallyпредложение будет выполнено только в случае openуспешного выполнения кода (без создания исключения), предполагая, что «отсутствие исключения» означает «успех» (как в случае с openPython). Если получение ресурса может завершиться неудачей без создания исключения, например, путем возврата формы null, это также необходимо проверить перед выпуском, например:

f  =  open ( имя файла ) попробуйте :  ... наконец :  if  f :  f . закрывать ()

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

с  открытым ( имя файла )  как  f :  ...

Вышеупомянутые методы — защита от раскручивания ( finally) и некоторая форма инкапсуляции — являются наиболее распространенным подходом к управлению ресурсами, встречающимся в различных формах в C#, Common Lisp , Java, Python, Ruby, Scheme и Smalltalk , [1] среди других; они датируются концом 1970-х годов на NIL- диалекте Лиспа; см. Обработка исключений § История . Существует множество вариантов реализации, а также существуют существенно разные подходы.

Подходы

Защита от раскручивания

Наиболее распространенным подходом к управлению ресурсами на разных языках является использование защиты от завершения, которая вызывается, когда выполнение выходит из области действия — когда выполнение выходит за пределы блока, возвращается из блока или генерируется исключение. Это работает для ресурсов, управляемых стеком, и реализовано на многих языках, включая C#, Common Lisp, Java, Python, Ruby и Scheme. Основные проблемы этого подхода заключаются в том, что код выпуска (чаще всего в finallyпредложении) может быть очень далек от кода получения (ему не хватает смежности ), и что код получения и выпуска всегда должен быть связан вызывающей стороной (в нем отсутствует инкапсуляция) . ). Их можно исправить либо функционально, используя замыкания/обратные вызовы/сопрограммы (Common Lisp, Ruby, Scheme), либо используя объект, который обрабатывает как получение, так и освобождение, и добавляя языковую конструкцию для вызова этих методов при входе и выходе элемента управления. область действия (C# using, Java tryс ресурсами, Python with); см. ниже.

Альтернативный, более императивный подход — написать асинхронный код в прямом стиле : получить ресурс, а затем в следующей строке выполнить отложенный выпуск, который вызывается при выходе из области действия — синхронное получение с последующим асинхронным выпуском. Он возник в C++ как класс ScopeGuard, созданный Андреем Александреску и Петру Маргиняном в 2000 году, [4] с улучшениями Джошуа Лерера, [5] и имеет прямую поддержку языка в D через scopeключевое слово (ScopeGuardStatement), где это один из подходов к безопасность исключений , в дополнение к RAII (см. ниже). [6] Он также был включен в Go как deferоператор. [7] В этом подходе отсутствует инкапсуляция – необходимо явно сопоставить получение и выпуск – но позволяет избежать необходимости создавать объект для каждого ресурса (с точки зрения кода избегайте написания класса для каждого типа ресурса).

Объектно-ориентированного программирования

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

Во-первых, возникает вопрос владения: есть ли у объекта ресурс?

Объекты, имеющие ресурс, могут приобретать и освобождать его по-разному, в разные моменты жизни объекта ; они встречаются парами, но на практике часто не используются симметрично (см. ниже):

Наиболее распространенным является получение ресурса во время создания объекта, а затем явное освобождение его с помощью метода экземпляра, обычно называемого dispose. Это аналогично традиционному управлению файлами (получение во время open, освобождение явным образом close) и известно как шаблон удаления . Это базовый подход, используемый в нескольких основных современных объектно-ориентированных языках, включая Java , C# и Python , и в этих языках имеются дополнительные конструкции для автоматизации управления ресурсами. Однако даже в этих языках более сложные объектные отношения приводят к более сложному управлению ресурсами, как описано ниже.

РАИИ

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

RAII — это стандартный подход к управлению ресурсами в C++, но он мало используется за пределами C++, несмотря на свою привлекательность, поскольку он плохо работает с современным автоматическим управлением памятью, в частности с отслеживанием сборки мусора : RAII связывает управление ресурсами с управлением памятью, но они имеют существенные различия. . Во-первых, поскольку ресурсы дороги, их желательно освобождать как можно скорее, поэтому объекты, содержащие ресурсы, должны быть уничтожены, как только они станут мусором (перестанут использоваться). Уничтожение объектов происходит быстро при детерминированном управлении памятью, например, в C++ (объекты, выделенные в стеке, уничтожаются при раскручивании стека, объекты, выделенные в куче, уничтожаются вручную с помощью вызова deleteили автоматически с помощью unique_ptr) или при детерминированном подсчете ссылок (где объекты уничтожаются немедленно, когда их счетчик ссылок падает до 0), и поэтому RAII хорошо работает в таких ситуациях. Однако большинство современных методов автоматического управления памятью являются недетерминированными и не дают никаких гарантий того, что объекты будут уничтожены быстро или даже вообще! Это связано с тем, что дешевле оставить некоторый мусор выделенным, чем собирать каждый объект сразу же, как только он становится мусором. Во-вторых, высвобождение ресурсов при уничтожении объекта означает, что у объекта должен быть финализатор (в детерминированном управлении памятью известный как деструктор ) — объект нельзя просто освободить — что существенно усложняет и замедляет сбор мусора.

Сложные отношения

Когда несколько объектов используют один ресурс, управление ресурсами может оказаться сложным.

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

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

С точки зрения реализации, в композиции объектов, если используется шаблон удаления, объект-владелец, таким образом, также будет иметь метод dispose, который, в свою очередь, вызывает disposeметоды принадлежащих объектов, которые должны быть удалены; в RAII это обрабатывается автоматически (при условии, что принадлежащие объекты сами автоматически уничтожаются: в C++, если они являются значением или unique_ptr, но не необработанным указателем: см. владение указателем). При агрегации объектов просматривающему объекту ничего делать не нужно, так как он не несет ответственности за ресурс.

Оба часто встречаются. Например, в библиотеке классов Java закрывает Reader#close()базовый поток, и их можно объединить в цепочку. Например, a BufferedReaderможет содержать InputStreamReader, который, в свою очередь, содержит FileInputStream, а вызов close, BufferedReaderв свою очередь, закрывает InputStreamReader, который, в свою очередь, закрывает FileInputStream, что, в свою очередь, освобождает ресурс системного файла. Действительно, объект, который напрямую использует ресурс, может быть даже анонимным благодаря инкапсуляции:

попробуйте ( BufferedReader Reader = новый BufferedReader ( новый InputStreamReader ( новый FileInputStream ( fileName ))))) { // Используйте Reader. } // Читатель закрывается при выходе из блока try-with-resources, который последовательно закрывает каждый из содержащихся объектов.         

Однако также можно управлять только тем объектом, который напрямую использует ресурс, и не использовать управление ресурсами для объектов-оболочек:

попробуйте ( поток FileInputStream = новый FileInputStream ( имя_файла )))) { читатель BufferedReader = новый BufferedReader ( новый InputStreamReader ( поток )); // Используем программу чтения. } // поток закрывается при выходе из блока try-with-resources. // Читатель больше не может использоваться после закрытия потока, но пока он не выходит за пределы блока, это не проблема.             

Напротив, в Python csv.reader не владеет объектом, fileкоторый он читает, поэтому нет необходимости (и невозможно) закрывать программу чтения, а вместо этого fileнеобходимо закрыть сам файл. [8]

с  открытым ( имя файла )  как  f :  r  =  csv . читатель ( f )  # Используйте r. # f закрывается при выходе из оператора with и больше не может использоваться. # С r ничего не делается, но базовый f закрыт, поэтому r тоже нельзя использовать.

В .NET соглашение заключается в том, чтобы ответственность возлагалась только на непосредственного пользователя ресурсов: «Вам следует реализовывать IDisposable только в том случае, если ваш тип напрямую использует неуправляемые ресурсы». [9]

В случае более сложного графа объектов , такого как несколько объектов, совместно использующих ресурс, или циклов между объектами, содержащими ресурсы, правильное управление ресурсами может быть довольно сложным, и возникают точно такие же проблемы, как и при финализации объекта (через деструкторы или финализаторы); например, проблема с истекшим прослушивателем может возникнуть и привести к утечке ресурсов при использовании шаблона наблюдателя (а наблюдатели удерживают ресурсы). Существуют различные механизмы, позволяющие лучше контролировать управление ресурсами. Например, в библиотеке Google Closure класс goog.Disposableпредоставляет registerDisposableметод для регистрации других объектов, которые будут удалены с помощью этого объекта, вместе с различными экземплярами и методами класса более низкого уровня для управления удалением.

Структурированное программирование

В структурированном программировании управление ресурсами стека осуществляется просто путем вложения кода, достаточного для обработки всех случаев. Это требует только одного возврата в конце кода и может привести к сильно вложенному коду, если необходимо получить много ресурсов, что некоторые считают антишаблоном - антишаблон Arrow [10] из-за треугольной формы. от последовательного вложения.

Пункт об очистке

Еще один подход, который допускает ранний возврат, но объединяет очистку в одном месте, состоит в том, чтобы иметь один возврат функции, которому предшествует код очистки, и использовать goto для перехода к очистке перед выходом. Это нечасто встречается в современном коде, но встречается в некоторых случаях использования C.

Смотрите также

Рекомендации

  1. ^ аб Бек 1997, стр. 37–39.
  2. ^ ab Elder, Jackson & Liblit 2008, с. 3.
  3. ^ Элдер, Джексон и Либлит 2008, с. 2.
  4. ^ «Общий: измените способ написания безопасного к исключениям кода — навсегда», Андрей Александреску и Петру Маргинян, 1 декабря 2000 г., Dr. Dobb's
  5. ^ ScopeGuard 2.0, Джошуа Лерер
  6. ^ D: Безопасность исключений
  7. Отложить, паниковать и восстановиться, Эндрю Джерранд, The Go Blog, 4 августа 2010 г.
  8. ^ Python: Нет csv.close()?
  9. ^ «IDisposable Интерфейс» . Проверено 3 апреля 2016 г.
  10. ^ Код выравнивания стрелок, Джефф Этвуд, 10 января 2006 г.

дальнейшее чтение

Внешние ссылки