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