La adquisición de recursos es inicialización ( RAII ) [1] es un modismo de programación [2] utilizado en varios lenguajes de programación orientados a objetos y de tipado estático para describir un comportamiento particular del lenguaje. En RAII, mantener un recurso es una invariante de clase y está ligado a la duración del objeto . La asignación (o adquisición) de recursos se realiza durante la creación del objeto (específicamente la inicialización), por el constructor , mientras que la desasignación (liberación) de recursos se realiza durante la destrucción del objeto (específicamente la finalización), por el destructor . En otras palabras, la adquisición de recursos debe tener éxito para que la inicialización tenga éxito. Por lo tanto, se garantiza que el recurso se mantenga entre el momento en que finaliza la inicialización y el momento en que comienza la finalización (mantener los recursos es una invariante de clase) y que se mantenga solo cuando el objeto esté vivo. Por lo tanto, si no hay fugas de objetos, no hay fugas de recursos .
RAII se asocia principalmente con C++ , donde se originó, pero también con Ada , [3] Vala , [4] y Rust . [5] La técnica fue desarrollada para la gestión de recursos segura ante excepciones en C++ [6] durante 1984-89, principalmente por Bjarne Stroustrup y Andrew Koenig , [7] y el término en sí fue acuñado por Stroustrup. [8]
Otros nombres para este modismo incluyen Constructor Acquires, Destructor Releases (CADRe) [9] y un estilo particular de uso se llama Scope-based Resource Management (SBRM). [10] Este último término es para el caso especial de variables automáticas . RAII vincula los recursos a la vida útil del objeto, que puede no coincidir con la entrada y salida de un ámbito. (Cabe destacar que las variables asignadas en el almacén libre tienen vidas útiles no relacionadas con ningún ámbito dado). Sin embargo, el uso de RAII para variables automáticas (SBRM) es el caso de uso más común.
El siguiente ejemplo de C++11 demuestra el uso de RAII para el acceso a archivos y el bloqueo de mutex :
#include <fstream> #include <iostream> #include <mutex> #include <stdexcept> #include <cadena> void WriteToFile ( const std :: string & message ) { // |mutex| es para proteger el acceso a |file| (que se comparte entre subprocesos). static std :: mutex mutex ; // Bloquea |mutex| antes de acceder a |archivo|. std :: lock_guard < std :: mutex > lock ( mutex ); // Intenta abrir el archivo. std :: ofstream file ( "example.txt" ); if ( ! file.is_open ( ) ) { throw std :: runtime_error ( "no se puede abrir el archivo" ); } // Escribe |mensaje| en |archivo|. archivo << mensaje << std :: endl ; // |file| se cerrará primero al salir del ámbito (sin importar la excepción) // |mutex| se desbloqueará en segundo lugar (desde el destructor |lock|) al salir del ámbito // (sin importar la excepción). }
Este código es seguro ante excepciones porque C++ garantiza que todos los objetos con duración de almacenamiento automática (variables locales) se destruyen al final del ámbito que los contiene en el orden inverso al de su construcción. [11] Por lo tanto, se garantiza que los destructores de los objetos de bloqueo y de archivo se llamarán al regresar de la función, independientemente de si se ha lanzado una excepción o no. [12]
Las variables locales permiten una gestión sencilla de múltiples recursos dentro de una única función: se destruyen en el orden inverso a su construcción, y un objeto se destruye solo si está completamente construido, es decir, si no se propaga ninguna excepción desde su constructor. [13]
El uso de RAII simplifica enormemente la gestión de recursos, reduce el tamaño total del código y ayuda a garantizar la corrección del programa. Por lo tanto, las pautas estándar de la industria recomiendan el uso de RAII [14] y la mayor parte de la biblioteca estándar de C++ sigue este modelo. [15]
Las ventajas de RAII como técnica de gestión de recursos son que proporciona encapsulación, seguridad de excepciones (para recursos de pila) y localidad (permite que la lógica de adquisición y liberación se escriba una al lado de la otra).
Se proporciona encapsulación porque la lógica de administración de recursos se define una vez en la clase, no en cada sitio de llamada. Se proporciona seguridad de excepción para los recursos de pila (recursos que se liberan en el mismo ámbito en el que se adquieren) al vincular el recurso a la duración de una variable de pila (una variable local declarada en un ámbito determinado): si se lanza una excepción y se implementa el manejo adecuado de excepciones, el único código que se ejecutará al salir del ámbito actual son los destructores de los objetos declarados en ese ámbito. Finalmente, se proporciona localidad de definición al escribir las definiciones de constructor y destructor una al lado de la otra en la definición de clase.
Por lo tanto, la gestión de recursos debe estar vinculada a la vida útil de los objetos adecuados para lograr una asignación y recuperación automáticas. Los recursos se adquieren durante la inicialización, cuando no hay posibilidad de que se utilicen antes de que estén disponibles, y se liberan con la destrucción de los mismos objetos, lo que se garantiza que se llevará a cabo incluso en caso de errores.
Al comparar RAII con la finally
construcción utilizada en Java, Stroustrup escribió que “En sistemas realistas, hay muchas más adquisiciones de recursos que tipos de recursos, por lo que la técnica 'la adquisición de recursos es inicialización' conduce a menos código que el uso de una construcción 'finalmente'”. [1]
El diseño RAII se utiliza a menudo para controlar bloqueos mutex en aplicaciones multiproceso . En ese uso, el objeto libera el bloqueo cuando se destruye. Sin RAII en este escenario, el potencial de interbloqueo sería alto y la lógica para bloquear el mutex estaría lejos de la lógica para desbloquearlo. Con RAII, el código que bloquea el mutex incluye esencialmente la lógica de que el bloqueo se liberará cuando la ejecución salga del alcance del objeto RAII.
Otro ejemplo típico es la interacción con archivos: podríamos tener un objeto que represente un archivo que esté abierto para escritura, en donde el archivo se abre en el constructor y se cierra cuando la ejecución sale del ámbito del objeto. En ambos casos, RAII solo garantiza que el recurso en cuestión se libere de manera apropiada; aún se debe tener cuidado para mantener la seguridad de excepciones. Si el código que modifica la estructura de datos o el archivo no es seguro para excepciones, el mutex podría desbloquearse o el archivo podría cerrarse con la estructura de datos o el archivo dañados.
La propiedad de objetos asignados dinámicamente (memoria asignada new
en C++) también se puede controlar con RAII, de modo que el objeto se libere cuando se destruya el objeto RAII (basado en pila). Para este propósito, la biblioteca estándar de C++11 define las clases de puntero inteligentestd::unique_ptr
para objetos de propiedad única y std::shared_ptr
para objetos con propiedad compartida. También hay clases similares disponibles std::auto_ptr
en C++98 y boost::shared_ptr
en las bibliotecas Boost .
Además, se pueden enviar mensajes a recursos de red mediante RAII. En este caso, el objeto RAII enviaría un mensaje a un socket al final del constructor, cuando se complete su inicialización. También enviaría un mensaje al comienzo del destructor, cuando el objeto esté a punto de ser destruido. Una construcción de este tipo se podría utilizar en un objeto cliente para establecer una conexión con un servidor que se ejecuta en otro proceso.
Tanto Clang como GNU Compiler Collection implementan una extensión no estándar del lenguaje C para soportar RAII: el atributo de variable "cleanup". [16] Lo siguiente anota una variable con una función destructora dada que llamará cuando la variable quede fuera de alcance:
void ejemplo_uso () { __atributo__ (( cleanup ( fclosep ))) ARCHIVO * archivo_de_registro = fopen ( "archivo_de_registro.txt" , "w+" ); fputs ( "¡Hola archivo_de_registro!" , archivo_de_registro ); }
En este ejemplo, el compilador organiza que se llame a la función fclosep en el archivo de registro antes de que example_usage regrese.
RAII solo funciona para recursos adquiridos y liberados (directa o indirectamente) por objetos asignados a la pila, donde hay un tiempo de vida de objeto estático bien definido. Los objetos asignados al montón que adquieren y liberan recursos son comunes en muchos lenguajes, incluido C++. RAII depende de que los objetos basados en el montón se eliminen implícita o explícitamente a lo largo de todas las rutas de ejecución posibles, para activar su destructor de liberación de recursos (o equivalente). [17] : 8:27 Esto se puede lograr utilizando punteros inteligentes para administrar todos los objetos del montón, con punteros débiles para objetos referenciados cíclicamente.
En C++, solo se garantiza que se produzca el desenrollado de la pila si se captura la excepción en algún lugar. Esto se debe a que "si no se encuentra ningún controlador coincidente en un programa, se llama a la función endsup(); independientemente de que la pila se desenrolle o no antes de esta llamada a endsup(), esto está definido por la implementación (15.5.1)" (estándar C++03, §15.3/9). [18] Este comportamiento suele ser aceptable, ya que el sistema operativo libera los recursos restantes, como memoria, archivos, sockets, etc., al finalizar el programa. [ cita requerida ]
En la conferencia Gamelab de 2018, Jonathan Blow explicó cómo el uso de RAII puede causar fragmentación de memoria , lo que a su vez puede causar errores de caché y una pérdida de rendimiento de 100 veces o peor . [19]
Perl , Python (en la implementación CPython ), [20] y PHP [21] administran la duración de los objetos mediante el conteo de referencias , lo que hace posible el uso de RAII. Los objetos a los que ya no se hace referencia se destruyen o finalizan y liberan inmediatamente, por lo que un destructor o finalizador puede liberar el recurso en ese momento. Sin embargo, no siempre es idiomático en dichos lenguajes y se desaconseja específicamente en Python (a favor de los administradores de contexto y finalizadores del paquete weakref ). [ cita requerida ]
Sin embargo, la duración de los objetos no está necesariamente ligada a ningún ámbito, y los objetos pueden ser destruidos de forma no determinista o no ser destruidos en absoluto. Esto hace posible la fuga accidental de recursos que deberían haber sido liberados al final de algún ámbito. Los objetos almacenados en una variable estática (en particular una variable global ) pueden no ser finalizados cuando el programa termina, por lo que sus recursos no son liberados; CPython no garantiza la finalización de dichos objetos, por ejemplo. Además, los objetos con referencias circulares no serán recolectados por un simple contador de referencias, y vivirán indeterminadamente; incluso si son recolectados (por una recolección de basura más sofisticada), el tiempo de destrucción y el orden de destrucción serán no deterministas. En CPython hay un detector de ciclos que detecta ciclos y finaliza los objetos en el ciclo, aunque antes de CPython 3.4, los ciclos no se recolectan si algún objeto en el ciclo tiene un finalizador. [22]