stringtranslate.com

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

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

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

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

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

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

Управление ресурсами направлено на контроль доступа с целью предотвращения обеих этих ситуаций.

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

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

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

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

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

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

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

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

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

Базовые техники

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

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

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

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

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

f  =  open ( имя_файла ) try :  ... finally :  f.close ( )

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

f  =  open ( имя_файла ) try :  ... finally :  if  f :  f.close ( )

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

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

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

Подходы

Защита от разматывания

Наиболее распространенным подходом к управлению ресурсами в разных языках является использование защиты от раскручивания, которая вызывается, когда выполнение выходит из области действия — выполнением, запущенным за пределами конца блока, возвратом из блока или выдачей исключения. Это работает для ресурсов, управляемых стеком, и реализовано во многих языках, включая C#, Common Lisp, Java, Python, Ruby и Scheme. Основные проблемы с этим подходом заключаются в том, что код освобождения (чаще всего в предложении finally) может быть очень далек от кода получения (ему не хватает смежности ), и что код получения и освобождения всегда должен быть связан вызывающей стороной (ему не хватает инкапсуляции ). Это можно исправить либо функционально, используя замыкания/обратные вызовы/сопрограммы (Common Lisp, Ruby, Scheme), либо используя объект, который обрабатывает как получение, так и освобождение, и добавляя языковую конструкцию для вызова этих методов, когда управление входит в область действия и выходит из нее (C# using, Java try-with-resources, 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метод, который, в свою очередь, вызывает disposeметоды принадлежащих объектов, которые должны быть удалены; в RAII это обрабатывается автоматически (при условии, что принадлежащие объекты сами автоматически уничтожаются: в C++, если они являются значением или unique_ptr, но не необработанным указателем: см. владение указателем). При агрегации объектов просматривающему объекту ничего не нужно делать, так как он не отвечает за ресурс.

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

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

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

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

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

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

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

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

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

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

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

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

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

Ссылки

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

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

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