Получение ресурса — это инициализация ( RAII ) [1] — это идиома программирования [2], используемая в нескольких объектно -ориентированных статически типизированных языках программирования для описания поведения конкретного языка. В RAII удержание ресурса является инвариантом класса и привязано к времени жизни объекта . Выделение (или получение) ресурса выполняется во время создания объекта (в частности, инициализации) конструктором , в то время как освобождение (освобождение) ресурса выполняется во время уничтожения объекта (в частности, финализации) деструктором . Другими словами, получение ресурса должно быть успешным для успешной инициализации. Таким образом, ресурс гарантированно будет удерживаться между окончанием инициализации и началом финализации (удержание ресурсов является инвариантом класса) и будет удерживаться только тогда, когда объект жив. Таким образом, если нет утечек объектов, нет и утечек ресурсов .
RAII ассоциируется в первую очередь с C++ , где он и возник, а также с Ada , [3] Vala , [4] и Rust . [5] Метод был разработан для безопасного управления ресурсами в C++ [6] в 1984–1989 годах, в первую очередь Бьярном Страуструпом и Эндрю Кёнигом , [7] а сам термин был придуман Страуструпом. [8]
Другие названия этой идиомы включают Constructor Acquires, Destructor Releases (CADRe) [9] , а один конкретный стиль использования называется Scope-based Resource Management (SBRM). [10] Последний термин относится к особому случаю автоматических переменных . RAII привязывает ресурсы к времени жизни объекта , которое может не совпадать с входом и выходом из области действия. (В частности, переменные, размещенные в свободном хранилище, имеют время жизни, не связанное с какой-либо заданной областью действия.) Однако использование RAII для автоматических переменных (SBRM) является наиболее распространенным вариантом использования.
Следующий пример C++11 демонстрирует использование RAII для доступа к файлам и блокировки мьютекса :
#include <fstream> #include <iostream> #include <mutex> #include <stdexcept> #include <string> void WriteToFile ( const std :: string & message ) { // |mutex| предназначен для защиты доступа к |файлу| (который является общим для потоков). static std :: mutex mutex ; // Блокируем |мьютекс| перед доступом к |файлу|. std :: lock_guard < std :: mutex > lock ( mutex ); // Попробовать открыть файл. std :: ofstream file ( "example.txt" ); if ( ! file.is_open ( ) ) { throw std :: runtime_error ( "unable to open file" ); } // Записать |сообщение| в |файл|. file << message << std :: endl ; // |file| будет закрыт первым при выходе из области видимости (независимо от исключения) // |mutex| будет разблокирован вторым (из деструктора |lock|) при выходе из области видимости // (независимо от исключения). }
Этот код является безопасным по отношению к исключениям, поскольку C++ гарантирует, что все объекты с автоматической продолжительностью хранения (локальные переменные) будут уничтожены в конце охватывающей области действия в обратном порядке их создания. [11] Таким образом, деструкторы как объектов блокировки , так и файловых объектов гарантированно будут вызваны при возврате из функции, независимо от того, было ли выброшено исключение или нет. [12]
Локальные переменные позволяют легко управлять несколькими ресурсами в рамках одной функции: они уничтожаются в порядке, обратном их созданию, а объект уничтожается только в том случае, если он полностью создан, то есть если из его конструктора не распространяется исключение. [13]
Использование RAII значительно упрощает управление ресурсами, уменьшает общий размер кода и помогает обеспечить корректность программы. Поэтому RAII рекомендуется отраслевыми стандартными руководствами [14] , и большая часть стандартной библиотеки C++ следует этой идиоме. [15]
Преимущества RAII как метода управления ресурсами заключаются в том, что он обеспечивает инкапсуляцию, безопасность исключений (для ресурсов стека) и локальность (позволяет записывать логику получения и освобождения рядом друг с другом).
Инкапсуляция обеспечивается, поскольку логика управления ресурсами определяется один раз в классе, а не в каждом месте вызова. Безопасность исключений обеспечивается для ресурсов стека (ресурсов, которые освобождаются в той же области, в которой они были получены) путем привязки ресурса к времени жизни переменной стека (локальной переменной, объявленной в данной области): если выбрасывается исключение и выполняется правильная обработка исключений, единственным кодом, который будет выполнен при выходе из текущей области, будут деструкторы объектов, объявленных в этой области. Наконец, локальность определения обеспечивается путем записи определений конструктора и деструктора рядом друг с другом в определении класса.
Поэтому управление ресурсами должно быть привязано к продолжительности жизни подходящих объектов, чтобы получить автоматическое распределение и возврат. Ресурсы приобретаются во время инициализации, когда нет никаких шансов на их использование до того, как они станут доступны, и освобождаются с уничтожением тех же объектов, что гарантированно произойдет даже в случае ошибок.
Сравнивая RAII с finally
конструкцией, используемой в Java, Страуструп писал, что «В реалистичных системах существует гораздо больше приобретений ресурсов, чем видов ресурсов, поэтому метод «получение ресурсов — это инициализация» приводит к меньшему количеству кода, чем использование конструкции «finally»» [1] .
Конструкция RAII часто используется для управления блокировками мьютекса в многопоточных приложениях. При таком использовании объект снимает блокировку при уничтожении. Без RAII в этом сценарии вероятность взаимоблокировки была бы высокой, а логика блокировки мьютекса была бы далека от логики его разблокировки. С RAII код, который блокирует мьютекс, по сути включает логику, согласно которой блокировка будет снята, когда выполнение покидает область действия объекта RAII.
Другой типичный пример — взаимодействие с файлами: у нас может быть объект, представляющий файл, открытый для записи, где файл открывается в конструкторе и закрывается, когда выполнение покидает область действия объекта. В обоих случаях RAII гарантирует только то, что рассматриваемый ресурс освобождается надлежащим образом; необходимо по-прежнему соблюдать осторожность для поддержания безопасности исключений. Если код, изменяющий структуру данных или файл, не является безопасным для исключений, мьютекс может быть разблокирован или файл может быть закрыт с поврежденной структурой данных или файлом.
Владение динамически выделяемыми объектами (память, выделяемая new
в C++) также может контролироваться с помощью RAII, так что объект освобождается при уничтожении объекта RAII (на основе стека). Для этой цели стандартная библиотека C++11 определяет классы интеллектуальных указателейstd::unique_ptr
для объектов с одним владельцем и std::shared_ptr
для объектов с общим владельцем. Аналогичные классы также доступны std::auto_ptr
в C++98 и boost::shared_ptr
в библиотеках Boost .
Также сообщения могут быть отправлены на сетевые ресурсы с использованием RAII. В этом случае объект RAII отправит сообщение в сокет в конце конструктора, когда его инициализация завершена. Он также отправит сообщение в начале деструктора, когда объект вот-вот будет уничтожен. Такая конструкция может использоваться в клиентском объекте для установления соединения с сервером, запущенным в другом процессе.
И Clang , и GNU Compiler Collection реализуют нестандартное расширение языка C для поддержки RAII: атрибут переменной «cleanup». [16] Следующий код аннотирует переменную с заданной функцией деструктора, которая будет вызвана, когда переменная выйдет из области видимости:
void example_usage () { __attribute__ (( cleanup ( fclosep ))) ФАЙЛ * logfile = fopen ( "logfile.txt" , "w+" ); fputs ( "привет logfile!" , logfile ); }
В этом примере компилятор организует вызов функции fclosep для файла журнала до возврата example_usage .
RAII работает только для ресурсов, полученных и освобожденных (прямо или косвенно) объектами, размещенными в стеке, где есть четко определенное время жизни статического объекта. Объекты, размещенные в куче , которые сами по себе получают и освобождают ресурсы, распространены во многих языках, включая C++. RAII зависит от объектов, размещенных в куче, которые должны быть неявно или явно удалены по всем возможным путям выполнения, чтобы вызвать его деструктор, освобождающий ресурсы (или эквивалент). [17] : 8:27 Этого можно достичь, используя умные указатели для управления всеми объектами кучи, со слабыми указателями для циклически ссылающихся объектов.
В C++ раскручивание стека гарантированно произойдет только в том случае, если где-то будет поймано исключение. Это происходит потому, что «Если в программе не найден соответствующий обработчик, вызывается функция terminate(); то, раскручивается ли стек перед вызовом terminate(), определяется реализацией (15.5.1)». (Стандарт C++03, §15.3/9). [18] Такое поведение обычно приемлемо, поскольку операционная система освобождает оставшиеся ресурсы, такие как память, файлы, сокеты и т. д. при завершении программы. [ требуется ссылка ]
На конференции Gamelab 2018 года Джонатан Блоу заявил, что использование RAII может привести к фрагментации памяти , что в свою очередь может привести к промахам кэша и снижению производительности в 100 и более раз . [19]
Perl , Python (в реализации CPython ), [20] и PHP [21] управляют временем жизни объекта с помощью подсчета ссылок , что позволяет использовать RAII. Объекты, на которые больше нет ссылок, немедленно уничтожаются или финализируются и освобождаются, поэтому деструктор или финализатор может освободить ресурс в это время. Однако это не всегда идиоматично в таких языках и специально не рекомендуется в Python (в пользу менеджеров контекста и финализаторов из пакета weakref ). [ необходима цитата ]
Однако время жизни объектов не обязательно привязано к какой-либо области действия, и объекты могут быть уничтожены недетерминированно или не уничтожены вообще. Это делает возможной случайную утечку ресурсов, которые должны были быть освобождены в конце некоторой области действия. Объекты, хранящиеся в статической переменной (особенно глобальной переменной ), могут не быть финализированы при завершении программы, поэтому их ресурсы не освобождаются; например, CPython не дает никаких гарантий финализации таких объектов. Кроме того, объекты с циклическими ссылками не будут собраны простым счетчиком ссылок и будут существовать неопределенно долго; даже если они будут собраны (более сложной сборкой мусора), время и порядок уничтожения будут недетерминированными. В CPython есть детектор циклов, который обнаруживает циклы и финализирует объекты в цикле, хотя до CPython 3.4 циклы не собирались, если какой-либо объект в цикле имел финализатор. [22]