La adquisición de recursos es la 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, la retención de un recurso es un invariante de clase y está ligada 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 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 retenga entre el final de la inicialización y el comienzo de la finalización (la retención de los recursos es un invariante de clase), y que se retenga solo mientras el objeto esté activo. 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 segura de recursos ante excepciones en C++ [ 6 ] durante 1984-1989, 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 Adquisiciones del Constructor, Liberaciones del Destructor (CADRe) [ 9 ] y un estilo de uso particular se llama Gestión de Recursos Basada en Ámbito (SBRM). [ 10 ] Este último término es para el caso especial de las variables automáticas . RAII vincula los recursos al ciclo de vida del objeto, que puede no coincidir con la entrada y salida de un ámbito. (Cabe destacar que las variables asignadas en el almacenamiento libre tienen ciclos de vida no relacionados 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.
Ejemplo de C++
El siguiente ejemplo demuestra el uso de RAII para el acceso a archivos y el bloqueo por exclusión mutua :
importar std ;using std :: mutex ; using std :: ofstream ; using std :: runtime_error ; using std :: scoped_lock ; using std :: string ;void writeToFile ( const string & message ) { // El mutex sirve para proteger el acceso al archivo (que se comparte entre hilos). static mutex m ;// Bloquear el mutex antes de acceder al archivo. scoped_lock < mutex > lock ( m );// Intentar abrir el archivo. ofstream f { "example.txt" }; if ( ! f . is_open ()) { throw runtime_error ( "no se puede abrir el archivo" ); }// Escribir mensaje en archivo. std :: println ( f , mensaje );// El archivo se cerrará primero al salir del ámbito (independientemente de la excepción). // El mutex se desbloqueará en segundo lugar (desde el destructor de bloqueo) al salir del ámbito // (independientemente de 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 producido una excepción o no. [ 12 ]
Las variables locales permiten una gestión sencilla de múltiples recursos dentro de una misma función: se destruyen en el orden inverso al de 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, RAII es recomendado por las directrices estándar de la industria, [ 14 ] y la mayor parte de la biblioteca estándar de C++ sigue este patrón. [ 15 ]
Beneficios
Las ventajas de RAII como técnica de gestión de recursos son que proporciona encapsulación, seguridad ante excepciones (para los recursos de la pila) y localidad (permite que la lógica de adquisición y liberación se escriba una al lado de la otra).
La encapsulación se logra porque la lógica de gestión de recursos se define una sola vez en la clase, no en cada llamada. La seguridad ante excepciones se garantiza para los recursos de la pila (recursos que se liberan en el mismo ámbito en que se adquieren) al vincular el recurso al ciclo de vida de una variable de pila (una variable local declarada en un ámbito determinado): si se produce una excepción y existe un manejo de excepciones adecuado, el único código que se ejecutará al salir del ámbito actual serán los destructores de los objetos declarados en ese ámbito. Finalmente, la localidad de definición se garantiza al escribir las definiciones del constructor y del destructor una al lado de la otra en la definición de la clase.
Por lo tanto, la gestión de recursos debe estar vinculada a la vida útil de los objetos adecuados para lograr la 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 estar disponibles, y se liberan con la destrucción de dichos objetos, lo cual está garantizado incluso en caso de errores.
Al comparar RAII con la finallyconstrucció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 'la adquisición de recursos es la inicialización' conduce a menos código que el uso de una construcción 'finally'”. [ 1 ]
Como invariante de clase, RAII proporciona garantías de que una instancia de objeto que se supone que ha adquirido un recurso, de hecho lo ha hecho. Esto elimina la necesidad de métodos de "configuración" adicionales para que un objeto recién creado esté en un estado utilizable (todo ese trabajo se realiza en el constructor; de manera similar, las tareas de "cierre" para liberar recursos ocurren en el destructor del objeto), y la necesidad de probar las instancias para verificar que se hayan configurado correctamente antes de cada uso. [ 16 ]
Usos típicos
El diseño RAII se utiliza frecuentemente para controlar bloqueos mutex en aplicaciones multihilo . En este caso, el objeto libera el bloqueo al ser destruido. Sin RAII, en este escenario, el riesgo de interbloqueo sería elevado y la lógica para bloquear el mutex estaría muy alejada de la lógica para desbloquearlo. Con RAII, el código que bloquea el mutex incluye la lógica que indica que el bloqueo se liberará cuando la ejecución salga del ámbito del objeto RAII.
Otro ejemplo típico es la interacción con archivos: podríamos tener un objeto que represente un archivo abierto para escritura, 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 adecuadamente; aun así, se debe tener cuidado para mantener la seguridad ante excepciones. Si el código que modifica la estructura de datos o el archivo no es seguro ante excepciones, el mutex podría desbloquearse o el archivo podría cerrarse, corrompiendo la estructura de datos o el archivo.
La propiedad de los objetos asignados dinámicamente (memoria asignada con newen C++) también se puede controlar con RAII, de modo que el objeto se libera cuando se destruye el objeto RAII (basado en pila). Para ello, la biblioteca estándar de C++11 define las clases de punteros inteligentesstd::unique_ptr para objetos de propiedad única y std::shared_ptrpara objetos con propiedad compartida. Clases similares también están disponibles a través de std::auto_ptren C++98 y boost::shared_ptren 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 finalizar el constructor, una vez completada su inicialización. También enviaría un mensaje al inicio del destructor, cuando el objeto esté a punto de ser destruido. Esta estructura podría utilizarse en un objeto cliente para establecer una conexión con un servidor que se ejecuta en otro proceso.
Disponer
En muchos lenguajes que carecen de gestión directa de memoria o desaconsejan su uso, se utiliza un mecanismo similar llamado "patrón dispose" para llamar a un disposemétodo que realiza la limpieza de recursos pertinente en el objeto al final de su ámbito.
do
Para las versiones de C anteriores a la introducción de Clang y la Colección de Compiladores GNU, implemente un atributo como una extensión no estándar de C. [ 17 ] Lo siguiente anota una variable con una función destructora determinada que llamará cuando la variable salga del ámbito:defer[[gnu::cleanup]]
#include <stdio.h> #include <time.h>void writeLogFile () { const char * logFileName = "logfile.txt" ;[[ gnu :: cleanup ( fclosep )]] FILE * logFile = fopen ( logFileName , "w+" );tiempo_t ahora = tiempo ( NULL );fprintf ( logFile , "Iniciando registro en %s a las %s" , filename , ctime ( & now )); }En este ejemplo, el compilador se encarga de que la fclosepfunción se llame logFileantes de writeLogFileque se produzca un retorno.
C++
En C++, la eliminación de objetos se realiza directamente mediante un destructor . En C++, una clase Xllama automáticamente a su destructor ~X()cuando llega al final de su ámbito. El patrón `dispose` es esencialmente equivalente a `RAII` en C++.
importar std ;using std :: ifstream ; using std :: string ;void readFile () { if ( ifstream reader { "story.txt" }; reader ) { string line ; while ( std :: getline ( reader , line )) { std :: println ( "{}" , line ); } } else { std :: println ( stderr , "Error al abrir el archivo" ); } // El lector se destruye después del bloque if/else }DO#
C# incluye un usingbloque -with-resources, que se puede usar si el objeto implementa System.IDisposable. Llamará a un Dispose()método al final del recurso.
using System ; using System.IO ;using ( StreamReader reader = new StreamReader ( "story.txt" )) { string line ; while (( line = reader . ReadLine ()) != null ) { Console . WriteLine ( line ); } } // El lector se libera automáticamente aquíJava
Java cuenta con un trybloque -with-resources, que se puede usar si el objeto implementa java.lang.AutoCloseable. Llamará a un close()método al final del recurso.
import java.io.BufferedReader ; import java.io.FileReader ; import java.io.IOException ;try ( BufferedReader reader = new BufferedReader ( new FileReader ( "story.txt" ))) { String line ; while (( line = reader . readLine ()) != null ) { System . out . println ( line ); } } catch ( IOException e ) { e . printStackTrace (); }Pitón
Python cuenta con un withbloque que se puede utilizar si el objeto implementa los métodos __enter__y __exit__.
con open ( "story.txt" , "r" ) como archivo : print ( archivo.readline ( )) # imprime la primera línea del archivo2Esto también se utiliza para gestionar recursos como los bloqueos .
from threading import Lockbalance_lock : Bloquear = Bloquear () con balance_lock : # Sección crítica: actualice el saldo de la cuenta aquí...Óxido
Rust permite definir una lógica de limpieza personalizada, si un objeto implementa std::ops::Drop, que llamará a un drop()método después de que el objeto salga del ámbito. Esto también puede ser llamado manualmente por std::mem::drop().
Limitaciones
RAII solo funciona para recursos adquiridos y liberados (directa o indirectamente) por objetos asignados en la pila, donde existe una vida útil de objeto estática bien definida. Los objetos asignados en el montón que a su vez 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 posibles rutas de ejecución, para activar su destructor de liberación de recursos (o equivalente). [ 18 ] : 8:27 Esto se puede lograr usando punteros inteligentes para administrar todos los objetos del montón, con punteros débiles para los objetos referenciados cíclicamente.
En C++, el desenrollado de la pila solo se garantiza si la excepción se captura en algún lugar. Esto se debe a que "Si no se encuentra ningún manejador coincidente en un programa, se llama a la función terminate(); si la pila se desenrolla o no antes de esta llamada a terminate() depende de la implementación (15.5.1)". (Estándar C++03, §15.3/9). [ 19 ] Este comportamiento suele ser aceptable, ya que el sistema operativo libera los recursos restantes, como memoria, archivos, sockets, etc., al finalizar el programa.
En la conferencia Gamelab de 2018, Jonathan Blow afirmó que el uso de RAII puede causar fragmentación de memoria , lo que a su vez puede provocar fallos de caché y una disminución del rendimiento de 100 veces o más . [ 20 ]
Conteo de referencias
Perl , Python (en la implementación CPython ) [ 21 ] y PHP [ 22 ] gestionan el ciclo de vida de los objetos mediante el conteo de referencias , lo que permite el uso de RAII. Los objetos que ya no se referencian se destruyen o finalizan y liberan inmediatamente, de modo que un destructor o finalizador puede liberar el recurso en ese momento. Sin embargo, esto no siempre es idiomático en dichos lenguajes, y se desaconseja específicamente en Python (en favor de los gestores de contexto y finalizadores del paquete weakref ).
Sin embargo, la vida útil de los objetos no está necesariamente ligada a ningún ámbito, y los objetos pueden destruirse de forma no determinista o no destruirse en absoluto. Esto puede provocar fugas accidentales de recursos que deberían haberse liberado al final de algún ámbito. Los objetos almacenados en una variable estática (en particular, una variable global ) pueden no finalizarse cuando el programa termina, 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 recolectados por un simple contador de referencias y vivirán indeterminadamente tiempo; incluso si se recolectan (mediante una recolección de basura más sofisticada), el tiempo y el orden de destrucción no serán deterministas. En CPython existe un detector de ciclos que detecta ciclos y finaliza los objetos en el ciclo, aunque antes de CPython 3.4, los ciclos no se recolectaban si algún objeto en el ciclo tenía un finalizador. [ 23 ]
Véase también
Referencias
- 1 2 Stroustrup, Bjarne (30-09-2017). "¿Por qué C++ no proporciona una construcción "finally"?" . Recuperado el 09-03-2019 .
- ↑ Sutter, Herb ; Alexandrescu, Andrei (2005). Estándares de codificación de C++ . Serie C++ en profundidad. Addison-Wesley. pág . 24. ISBN 978-0-321-11358-0.
- ↑ "Gema n.º 70: El modismo de bloqueo de alcance" . AdaCore . Consultado el 21 de mayo de 2021 .
- ↑ El proyecto Valadate. "Destrucción" . El tutorial de Vala versión 0.30 . Consultado el 21 de mayo de 2021 .
- ↑ "RAII - Rust By Example" . doc.rust-lang.org . Consultado el 22 de noviembre de 2020 .
- ↑ Stroustrup 1994 , 16.5 Gestión de recursos, págs. 388–89.
- ↑ Stroustrup 1994 , 16.1 Manejo de excepciones: Introducción, págs. 383–84.
- ↑ Stroustrup 1994 , p. 389. Llamé a esta técnica "la adquisición de recursos es la inicialización".
- ↑ Arthur Tchaikovsky (06-11-2012). "Cambiar RAII oficial a CADRe" . Estándar ISO C++ - Propuestas futuras . Grupos de Google . Consultado el 09-03-2019 .
- ^ Chou, Allen (1 de octubre de 2014). "Gestión de recursos basada en el alcance (RAII)" . Consultado el 9 de marzo de 2019 .
- ↑ Richard Smith (21 de marzo de 2017). "Borrador de trabajo, estándar para el lenguaje de programación C++" (PDF) . pág. 151, sección §9.6 . Consultado el 7 de septiembre de 2023 .
- ↑ "¿Cómo puedo manejar un destructor que falla?" . Standard C++ Foundation . Consultado el 09-03-2019 .
- ↑ Richard Smith (21 de marzo de 2017). "Borrador de trabajo, estándar para el lenguaje de programación C++" (PDF) . Consultado el 9 de marzo de 2019 .
- ↑ Stroustrup, Bjarne ; Sutter, hierba (3 de agosto de 2020). "Pautas básicas de C++" . Consultado el 15 de agosto de 2020 .
- ↑ "Tengo demasiados bloques try; ¿qué puedo hacer al respecto?" . Standard C++ Foundation . Consultado el 9 de marzo de 2019 .
- ↑ RAII en cppreference.com
- ↑ "Especificación de atributos de variables" . Uso de la colección de compiladores GNU (GCC) . Proyecto GNU . Consultado el 9 de marzo de 2019 .
- ↑ Weimer, Westley; Necula, George C. (2008). "Situaciones excepcionales y fiabilidad de los programas" (PDF) . ACM Transactions on Programming Languages and Systems . Vol. 30, n.º 2.
- ↑ ildjarn (2011-04-05). "RAII y desenrollado de pila" . Stack Overflow . Recuperado el 2019-03-09 .
- ↑ Gamelab2018 - Decisiones de diseño de Jon Blow para crear Jai, un nuevo lenguaje para programadores de videojuegos en YouTube
- ↑ "Extendiendo Python con C o C++: Conteo de referencias" . Extendiendo e integrando el intérprete de Python . Python Software Foundation . Consultado el 9 de marzo de 2019 .
- ↑ hobbs (2011-02-08). "¿PHP admite el patrón RAII? ¿Cómo?" . Consultado el 2019-03-09 .
- ↑ "gc — Interfaz del recolector de basura" . La biblioteca estándar de Python . Python Software Foundation . Consultado el 9 de marzo de 2019 .
Lecturas adicionales
- Stroustrup, Bjarne (1994). El diseño y la evolución de C++ . Addison-Wesley. Bibcode : 1994dic..book.....S . ISBN 978-0-201-54330-8.
Enlaces externos
- Capítulo de muestra: " Error común n.° 67: No emplear la adquisición de recursos es la inicialización " por Stephen C. Dewhurst
- Entrevista: " Una conversación con Bjarne Stroustrup " por Bill Venners
- Artículo: " La ley de los dos grandes " por Bjorn Karlsson y Matthew Wilson
- Artículo: " Implementación del modismo 'La adquisición de recursos es la inicialización' " por Danny Kalev
- Artículo: " RAII, objetos dinámicos y fábricas en C++ " por Roland Pibinger
- RAII en Delphi: " RAII en Delphi en una sola línea " por Barry Kelly
- Guía: RAII en C++ por W3computing
- Programación orientada a objetos
- patrones de diseño de software
- Modismos de programación
- C++
