La adquisición de recursos es inicialización ( RAII ) [1] es un modismo de programación [2] utilizado en varios lenguajes de programación de tipado estático y orientado a objetos para describir el comportamiento de un lenguaje particular. En RAII, mantener un recurso es una invariante de clase y está vinculado a la vida útil del objeto . La asignación (o adquisición) de recursos se realiza durante la creación del objeto (específicamente la inicialización), por parte del constructor , mientras que la desasignación (liberación) de recursos se realiza durante la destrucción del objeto (específicamente la finalización), por parte del 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 mantendrá entre el momento en que finaliza la inicialización y el inicio de la finalización (retener los recursos es una invariante de clase) y se mantendrá solo cuando el objeto esté vivo. Por lo tanto, si no hay fugas de objetos, no hay fugas de recursos .
RAII se asocia más prominentemente 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 de uso particular se llama Gestión de recursos basada en alcance (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 alcance. (En particular, las variables asignadas en la tienda gratuita tienen vidas útiles que no están relacionadas con ningún ámbito determinado). 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 mutex :
#include <fstream> #include <iostream> #include <mutex> #include <stdexcept> #include <cadena> void WriteToFile ( const std :: cadena y mensaje ) { // |mutex| es proteger el acceso a |archivo| (que se comparte entre hilos). estándar estático :: mutex mutex ; // Bloquear |mutex| antes de acceder a |archivo|. std :: lock_guard < std :: mutex > bloqueo ( mutex ); // Intenta abrir el archivo. std :: archivo ofstream ( "ejemplo.txt" ); if ( ! file . is_open ()) { throw std :: runtime_error ( "no se puede abrir el archivo" ); } // Escribir |mensaje| a |archivar|. archivo << mensaje << std :: endl ; // |archivo| se cerrará primero al salir del alcance (independientemente de la excepción) // |mutex| se desbloqueará en segundo lugar (desde |lock| destructor) al salir del alcance // (independientemente de la excepción). }
Este código es seguro para excepciones porque C++ garantiza que todos los objetos con duración de almacenamiento automático (variables locales) se destruyen al final del alcance adjunto en el orden inverso a su construcción. [11] Por lo tanto, se garantiza que los destructores de los objetos de bloqueo y de archivo serán llamados al regresar de la función, ya sea que se haya lanzado una excepción o no. [12]
Las variables locales permiten una fácil gestión de múltiples recursos dentro de una sola 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 general del código y ayuda a garantizar la corrección del programa. Por lo tanto, RAII es recomendado por las pautas estándar de la industria, [14] y la mayor parte de la biblioteca estándar de C++ sigue este idioma. [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 escribir la lógica de adquisición y liberación 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 la pila (recursos que se liberan en el mismo ámbito en el que se adquieren) vinculando el recurso a la vida útil de una variable de la pila (una variable local declarada en un ámbito determinado): si se lanza una excepción , y Si se implementa un manejo de excepciones adecuado, el único código que se ejecutará al salir del alcance actual son los destructores de los objetos declarados en ese alcance. Finalmente, la localidad de definición se proporciona escribiendo 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 poder obtener 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 producirá incluso en caso de errores.
Comparando 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 de '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 de subprocesos múltiples . 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 abandone el 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, donde el archivo se abre en el constructor y se cierra cuando la ejecución sale del alcance del objeto. En ambos casos, la RAII sólo garantiza que el recurso en cuestión se libere adecuadamente; aún se debe tener cuidado para mantener la seguridad excepcional. Si el código que modifica la estructura de datos o el archivo no es seguro para excepciones, el mutex podría desbloquearse o cerrarse el archivo con la estructura de datos o el archivo dañado.
La propiedad de los objetos asignados dinámicamente (memoria asignada en new
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 ello, la biblioteca estándar 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 de 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 podría usarse en un objeto de 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 admitir RAII: el atributo variable de "limpieza". [16] Lo siguiente anota una variable con una función destructora dada que llamará cuando la variable salga del alcance:
void example_usage () { __attribute__ (( limpieza ( fclosep ))) ARCHIVO * logfile = fopen ( "logfile.txt" , "w+" ); fputs ( "¡hola archivo de registro!" , archivo de registro ); }
En este ejemplo, el compilador organiza la llamada a la función fclosep en el archivo de registro antes de que regrese example_usage .
RAII solo funciona para recursos adquiridos y liberados (directa o indirectamente) por objetos asignados en pila, donde existe una vida útil de objeto estático bien definida. Los objetos asignados en 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 mediante el uso de punteros inteligentes para administrar todos los objetos del montón, con punteros débiles para los objetos a los que se hace referencia cíclicamente.
En C++, solo se garantiza que la pila se desenrolle si la excepción se detecta 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 terminar(); si la pila se desenrolla o no antes de esta llamada a terminar() 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 necesaria ]
En la conferencia Gamelab de 2018, Jonathan Blow explicó cómo el uso de RAII puede causar fragmentación de la memoria , lo que a su vez puede provocar errores de caché y un impacto 100 veces o peor en el rendimiento . [19]
Perl , Python (en la implementación CPython ), [20] y PHP [21] gestionan la vida útil de los objetos mediante el recuento de referencias , lo que hace posible utilizar 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 débilref ). [ cita necesaria ]
Sin embargo, la vida útil de los objetos no está necesariamente sujeta a ningún alcance y los objetos pueden destruirse de forma no determinista o no destruirse en absoluto. Esto hace posible que se filtren accidentalmente recursos que deberían haberse liberado al final de algún alcance. Los objetos almacenados en una variable estática (en particular, una variable global ) pueden no finalizarse cuando finaliza el programa, por lo que sus recursos no se liberan; CPython no garantiza la finalización de dichos objetos, por ejemplo. Además, los objetos con referencias circulares no serán recopilados por un simple contador de referencias y vivirán indeterminadamente; incluso si se recolecta (mediante una recolección de basura más sofisticada), el tiempo y el orden de destrucción no serán deterministas. En CPython hay un detector de ciclos que detecta ciclos y finaliza los objetos del ciclo, aunque antes de CPython 3.4, los ciclos no se recopilan si algún objeto del ciclo tiene un finalizador. [22]