Articulo de referencia

Gestión de recursos (informática)

En programación informática , la gestión de recursos se refiere a las técnicas para administrar recursos (componentes con disponibilidad limitada). Los programas informáticos pu...

En programación informática , la gestión de recursos se refiere a las técnicas para administrar recursos (componentes con disponibilidad limitada).

Los programas informáticos pueden gestionar sus propios recursos utilizando las características que ofrecen los lenguajes de programación ( Elder, Jackson y Liblit (2008) es un artículo de revisión que compara diferentes enfoques), o pueden optar por gestionarlos mediante un anfitrión (un sistema operativo o una máquina virtual ) u otro programa.

La gestión basada en el host se conoce como seguimiento de recursos y consiste en limpiar las fugas de recursos: finalizar el acceso a los recursos que se han adquirido pero no se han liberado después de su uso. Esto se conoce como recuperación de recursos y es análogo a la recolección de basura para la memoria. En muchos sistemas, el sistema operativo recupera los recursos después de que el proceso realiza la llamada al sistema exit .

Controlar el acceso

La omisión de liberar un recurso cuando un programa ha terminado de usarlo se conoce como fuga de recursos y es un problema en la computación secuencial. El hecho de que varios procesos deseen acceder a un recurso limitado puede ser un problema en la computación concurrente y se conoce como contención de recursos .

La gestión de recursos busca controlar el acceso para prevenir ambas situaciones.

fuga de recursos

Formalmente, la gestión de recursos (prevención de fugas de recursos) consiste en asegurar que un recurso se libere si y solo si se adquiere con éxito. Este problema general se puede abstraer como código " antes, cuerpo y después ", que normalmente se ejecuta en este orden, con la condición de que el código después se llama si y solo si el código antes se completa con éxito, independientemente de si el código cuerpo se ejecuta con éxito o no. Esto también se conoce como ejecución alrededor [ 1 ] o un sándwich de código, y ocurre en varios otros contextos, [ 2 ] como un cambio temporal de estado del programa, o el rastreo de entrada y salida en una subrutina . Sin embargo, la gestión de recursos es la aplicación más citada. En la programación orientada a aspectos , dicha lógica de ejecución alrededor es una forma de consejo .

En la terminología del análisis del flujo de control , la liberación de recursos debe ser posterior a la adquisición exitosa de recursos; [ 3 ] no garantizar esto es un error, y una ruta de código que viola esta condición causa una fuga de recursos. Las fugas de recursos suelen ser problemas menores, que generalmente no bloquean el programa, sino que causan cierta ralentización del programa o del sistema en general. [ 2 ] Sin embargo, pueden causar bloqueos, ya sea del propio programa o de otros programas, debido al agotamiento de recursos: si el sistema se queda sin recursos, las solicitudes de adquisición fallan. Esto puede presentar un fallo de seguridad si un ataque puede causar el agotamiento de recursos. Las fugas de recursos pueden ocurrir durante el flujo normal del programa, como simplemente olvidar liberar un recurso, o solo en circunstancias excepcionales, como cuando un recurso no se libera si hay una excepción en otra parte del programa. Las fugas de recursos son causadas con mucha frecuencia por la salida anticipada de una subrutina, ya sea por una returninstrucción o por una excepción generada por la propia subrutina o por una subrutina más profunda a la que llama. Si bien la liberación de recursos debido a las instrucciones de retorno se puede manejar liberándolos cuidadosamente dentro de la subrutina antes del retorno, las excepciones no se pueden manejar sin alguna facilidad de lenguaje adicional que garantice que se ejecute el código de liberación.

De forma más sutil, la adquisición exitosa de recursos debe dominar la liberación de recursos, ya que de lo contrario el código intentará liberar un recurso que no ha adquirido. Las consecuencias de una liberación incorrecta de este tipo van desde ser ignorada silenciosamente hasta el bloqueo del programa o un comportamiento impredecible. Estos errores generalmente se manifiestan raramente, ya que requieren que la asignación de recursos falle primero, lo cual suele ser un caso excepcional. Además, las consecuencias pueden no ser graves, ya que el programa puede estar fallando debido a la imposibilidad de adquirir un recurso esencial. Sin embargo, estos errores pueden impedir la recuperación del fallo o convertir un cierre ordenado en uno desordenado. Esta condición generalmente se garantiza verificando primero que el recurso se haya adquirido correctamente antes de liberarlo, ya sea mediante una variable booleana que registre "adquirido correctamente" (lo cual carece de atomicidad si el recurso se adquiere pero la variable de indicador no se actualiza, o viceversa) o mediante el identificador del recurso de un tipo anulable , donde "nulo" indica "no adquirido correctamente", lo que garantiza la atomicidad.

contención de recursos

En informática, la contención de recursos se refiere a un conflicto que surge cuando varias entidades intentan acceder a un recurso compartido, como la memoria de acceso aleatorio, el almacenamiento en disco, la memoria caché, los buses internos o los dispositivos de red externos.

Gestión de la memoria

La memoria puede tratarse como un recurso, pero su gestión suele considerarse por separado, principalmente porque la asignación y liberación de memoria es mucho más frecuente que la adquisición y liberación de otros recursos, como los descriptores de archivo. La memoria gestionada por un sistema externo presenta similitudes tanto con la gestión de memoria (interna) (ya que es memoria) como con la gestión de recursos (ya que la gestiona un sistema externo). Algunos ejemplos son la memoria gestionada mediante código nativo y utilizada desde Java (a través de la Interfaz Nativa de Java ) y los objetos del Modelo de Objetos del Documento (DOM), utilizados desde JavaScript . En ambos casos, el gestor de memoria ( recolector de basura ) del entorno de ejecución (máquina virtual) no puede gestionar la memoria externa (no existe gestión de memoria compartida), por lo que esta se trata como un recurso y se gestiona de forma análoga. Sin embargo, los ciclos entre sistemas (JavaScript que hace referencia al DOM y viceversa) pueden dificultar o imposibilitar la gestión.

Gestión léxica y gestión explícita

Una distinción clave en la gestión de recursos dentro de un programa radica en la diferencia entre la gestión léxica y la gestión explícita : si un recurso puede manejarse como si tuviera un ámbito léxico, como una variable de pila (cuya vida útil se limita a un único ámbito léxico, adquiriéndose al entrar o dentro de un ámbito específico y liberándose al finalizar la ejecución de dicho ámbito), o si un recurso debe asignarse y liberarse explícitamente, como un recurso adquirido dentro de una función y luego devuelto por ella, que posteriormente debe liberarse fuera de la función que lo adquirió. La gestión léxica, cuando es aplicable, permite una mejor separación de responsabilidades y es menos propensa a errores.

Técnicas básicas

El enfoque básico para la gestión de recursos es adquirir un recurso, hacer algo con él y luego liberarlo, generando un código de la forma (ilustrado con la apertura de un archivo en Python ):

from typing import TextIOf : TextIO = abrir ( nombre_archivo ) ... f.cerrar ( )

Esto es correcto si el ...código intermedio no contiene una salida anticipada ( return), el lenguaje no tiene excepciones y opense garantiza el éxito. Sin embargo, provoca una fuga de recursos si hay un retorno o una excepción, y causa una liberación incorrecta de recursos no adquiridos si openpuede fallar.

Existen dos problemas fundamentales adicionales: la adquisición y la liberación no están contiguas (el código de liberación debe escribirse lejos del código de adquisición), y la gestión de recursos no está encapsulada; el programador debe asegurarse manualmente de que siempre estén emparejadas. En conjunto, esto implica que la adquisición y la liberación deben estar explícitamente emparejadas, pero no pueden ubicarse juntas, lo que facilita que no se emparejen correctamente.

La fuga de recursos se puede resolver en lenguajes que admiten una finallyconstrucción (como Python) colocando el cuerpo en una trycláusula y la liberación en una finallycláusula:

from typing import TextIOf : TextIO = abrir ( nombre_archivo ) intentar : ... finalmente : f.cerrar ( )

Esto garantiza una liberación correcta incluso si hay un retorno dentro del cuerpo o se lanza una excepción. Además, tenga en cuenta que la adquisición ocurre antes de la trycláusula, lo que garantiza que la finallycláusula solo se ejecute si el opencódigo tiene éxito (sin lanzar una excepción), asumiendo que "sin excepción" significa "éxito" (como es el caso openen Python). Si la adquisición de recursos puede fallar sin lanzar una excepción, como al devolver una forma de null, también debe verificarse antes de la liberación, como por ejemplo:

from typing import TextIOf : TextIO = abrir ( nombre_archivo ) intentar : ... finalmente : si f : f.cerrar ( )

Si bien esto garantiza una gestión correcta de los recursos, no proporciona adyacencia ni encapsulación. En muchos lenguajes existen mecanismos que proporcionan encapsulación, como la withinstrucción en Python:

con open ( nombre de archivo ) como f : ...

Las técnicas anteriores – protección de desenrollado ( finally) y alguna forma de encapsulación – son el enfoque más común para la gestión de recursos, que se encuentra en varias formas en C# , Common Lisp , Java , Python , Ruby , Scheme y Smalltalk , [ 1 ] entre otros; se remontan a finales de la década de 1970 en el dialecto NIL de Lisp; véase Manejo de excepciones §  Historia . Hay muchas variaciones en la implementación, y también hay enfoques significativamente diferentes .

Aproches

Protección de desenrollado

El enfoque más común para la gestión de recursos en distintos lenguajes es el uso de la protección de desenrollado, que se llama cuando la ejecución sale de un ámbito, ya sea porque la ejecución sale del bloque, regresa desde dentro del bloque o se lanza una excepción. Esto funciona para recursos gestionados por pila y está implementado en muchos lenguajes, incluidos C#, Common Lisp, Java, Python, Ruby y Scheme. Los principales problemas de este enfoque son que el código de liberación (generalmente en una finallycláusula) puede estar muy alejado del código de adquisición (carece de adyacencia ) y que el código de adquisición y liberación siempre debe estar emparejado por quien lo llama (carece de encapsulación ). Esto se puede solucionar funcionalmente, usando cierres/devoluciones de llamada/corutinas (Common Lisp, Ruby, Scheme), o usando un objeto que gestione tanto la adquisición como la liberación, y añadiendo una construcción del lenguaje para llamar a estos métodos cuando el control entra y sale de un ámbito (C# using, Java trycon recursos, Python with); véase más abajo.

Un enfoque alternativo, más imperativo, es escribir código asíncrono en estilo directo : adquirir un recurso y luego en la siguiente línea tener una liberación diferida , que se llama cuando se sale del ámbito – adquisición síncrona seguida de liberación asíncrona. Esto se originó en C++ como la clase ScopeGuard, por Andrei Alexandrescu y Petru Marginean en 2000, [ 4 ] con mejoras de Joshua Lehrer, [ 5 ] y tiene soporte directo del lenguaje en D a través de la scopepalabra clave ( ScopeGuardStatement ), donde es un enfoque para la seguridad de excepciones , además de RAII (ver más abajo). [ 6 ] También se ha incluido en Go, como la deferinstrucción. [ 7 ] Este enfoque carece de encapsulación – uno debe hacer coincidir explícitamente la adquisición y la liberación – pero evita tener que crear un objeto para cada recurso (en términos de código, evitar escribir una clase para cada tipo de recurso).

Programación orientada a objetos

En la programación orientada a objetos , los recursos se encapsulan dentro de los objetos que los utilizan, como por ejemplo un fileobjeto con un campo cuyo valor es un descriptor de archivo (o, más generalmente, un identificador de archivo ). Esto permite que el objeto utilice y gestione el recurso sin que los usuarios del objeto tengan que hacerlo. Sin embargo, existen diversas formas en que los objetos y los recursos pueden relacionarse.

En primer lugar, está la cuestión de la propiedad: ¿ tiene un objeto un recurso?

Los objetos que poseen un recurso pueden adquirirlo y liberarlo de diferentes maneras, en diferentes momentos durante la vida útil del objeto ; estos ocurren en pares, pero en la práctica a menudo no se utilizan de forma simétrica (véase más abajo):

  • Adquirir/liberar mientras el objeto sea válido, mediante métodos (de instancia) como openo dispose.
  • Adquirir/liberar durante la creación/destrucción de objetos (en el inicializador y el finalizador).
  • Ni se adquiere ni se libera el recurso, sino que simplemente se tiene una vista o referencia a un recurso gestionado externamente al objeto, como en la inyección de dependencias ; concretamente, un objeto que tiene un recurso (o puede comunicarse con uno que lo tiene) se pasa como argumento a un método o constructor.

Lo más habitual es adquirir un recurso durante la creación del objeto y liberarlo explícitamente mediante un método de instancia, comúnmente llamado `dispose` dispose. Esto es análogo a la gestión de archivos tradicional (adquirir durante la creación del objeto openy liberarlo explícitamente close), y se conoce como el patrón `dispose` . Este es el enfoque básico utilizado en varios lenguajes modernos de programación orientada a objetos, como Java , C# y Python , y estos lenguajes cuentan con construcciones adicionales para automatizar la gestión de recursos. Sin embargo, incluso en estos lenguajes, las relaciones entre objetos más complejas dan lugar a una gestión de recursos más compleja, como se explica más adelante.

RAII

Un enfoque natural consiste en hacer que la retención de un recurso sea una invariante de clase : los recursos se adquieren durante la creación del objeto (específicamente la inicialización) y se liberan durante la destrucción del objeto (específicamente la finalización). Esto se conoce como Adquisición de Recursos durante la Inicialización (RAII, por sus siglas en inglés) y vincula la gestión de recursos con el ciclo de vida del objeto , asegurando que los objetos activos tengan todos los recursos necesarios. Otros enfoques no hacen que la retención del recurso sea una invariante de clase, por lo que los objetos pueden carecer de los recursos necesarios (porque aún no se han adquirido, ya se han liberado o se gestionan externamente), lo que provoca errores como intentar leer un archivo cerrado. Este enfoque vincula la gestión de recursos con la gestión de memoria (específicamente la gestión de objetos), de modo que si no hay fugas de memoria (ni fugas de objetos), no hay fugas de recursos . RAII funciona de forma natural para los recursos gestionados por el montón, no solo para los gestionados por la pila, y es componible: los recursos que poseen los objetos en relaciones arbitrariamente complejas (un grafo de objetos complejo ) se liberan de forma transparente simplemente mediante la destrucción del objeto (¡siempre que esto se haga correctamente!).

RAII es el enfoque estándar de gestión de recursos en C++, pero se usa poco fuera de C++, a pesar de su atractivo, porque funciona mal con la gestión automática de memoria moderna, específicamente con la recolección de basura por rastreo : RAII vincula la gestión de recursos con la gestión de memoria, pero estas tienen diferencias significativas. En primer lugar, como los recursos son costosos, es deseable liberarlos rápidamente, por lo que los objetos que contienen recursos deben destruirse tan pronto como se convierten en basura (ya no están en uso). La destrucción de objetos es inmediata en la gestión de memoria determinista, como en C++ (los objetos asignados en la pila se destruyen al desenrollar la pila, los objetos asignados en el montón se destruyen manualmente mediante una llamada deleteo automáticamente usando unique_ptr) o en el conteo de referencias determinista (donde los objetos se destruyen inmediatamente cuando su contador de referencias cae a 0), y por lo tanto RAII funciona bien en estas situaciones. Sin embargo, la mayoría de la gestión automática de memoria moderna no es determinista, ¡no hace garantías de que los objetos se destruirán rápidamente o incluso en absoluto! Esto se debe a que es más barato dejar algo de basura asignada que recolectar con precisión cada objeto inmediatamente cuando se convierte en basura. En segundo lugar, liberar recursos durante la destrucción de un objeto implica que este debe tener un finalizador (conocido como destructor en la gestión de memoria determinista ), ya que el objeto no puede simplemente ser desasignado, lo que complica y ralentiza significativamente la recolección de basura.

Relaciones complejas

Cuando varios objetos dependen de un único recurso, la gestión de recursos puede resultar complicada.

Una pregunta fundamental es si una relación "tiene un" implica poseer otro objeto ( composición de objetos ) o visualizarlo ( agregación de objetos ). Un caso común es cuando dos objetos se encadenan, como en los patrones de tubería y filtro , delegación , decorador o adaptador . Si el segundo objeto (que no se usa directamente) contiene un recurso, ¿es el primer objeto (que sí se usa directamente) responsable de gestionarlo? Generalmente, la respuesta es idéntica a la de si el primer objeto posee al segundo: si es así, el objeto propietario también es responsable de la gestión del recurso ("tener un recurso" es transitivo ); de lo contrario, no lo es. Además, un solo objeto puede "tener" varios otros objetos, poseyendo algunos y visualizando otros.

Ambos casos son comunes y las convenciones difieren. Si los objetos que utilizan recursos son responsables indirectamente del recurso (composición), se logra la encapsulación (solo se necesita el objeto que utilizan los clientes, sin objetos separados para los recursos), pero esto genera una complejidad considerable, especialmente cuando un recurso es compartido por varios objetos o cuando estos tienen relaciones complejas. Si solo el objeto que utiliza directamente el recurso es responsable del mismo (agregación), se pueden ignorar las relaciones entre otros objetos que utilizan el recurso, pero no hay encapsulación (más allá del objeto que lo utiliza directamente): el recurso debe gestionarse directamente y podría no estar disponible para el objeto que lo utiliza indirectamente (si se ha liberado por separado).

En cuanto a la implementación, en la composición de objetos, si se utiliza el patrón `dispose`, el objeto propietario también tendrá un disposemétodo que, a su vez, llama a los disposemétodos de los objetos propiedad de los que se debe liberar el recurso; en RAII esto se gestiona automáticamente (siempre que los objetos propiedad de los que se posee el recurso se destruyan automáticamente: en C++ si son un valor o un puntero unique_ptr, pero no un puntero sin formato: véase propiedad de punteros ). En la agregación de objetos, el objeto que visualiza el recurso no necesita hacer nada, ya que no es responsable del recurso.

Ambos son comunes. Por ejemplo, en la biblioteca de clases de Java , Reader#close()cierra el flujo subyacente, y estos se pueden encadenar. Por ejemplo, un BufferedReaderpuede contener un InputStreamReader, que a su vez contiene un FileInputStream, y al llamar closea en el BufferedReadercierra el InputStreamReader, que a su vez cierra el FileInputStream, que a su vez libera el recurso del archivo del sistema. De hecho, el objeto que usa directamente el recurso puede incluso ser anónimo, gracias a la encapsulación:

try ( BufferedReader reader = new BufferedReader ( new InputStreamReader ( new FileInputStream ( fileName )))) { // Usar el lector. } // El lector se cierra cuando se sale del bloque try-with-resources, que cierra cada uno de los objetos contenidos en secuencia.

Sin embargo, también es posible gestionar únicamente el objeto que utiliza directamente el recurso, y no utilizar la gestión de recursos en los objetos contenedores:

try ( FileInputStream stream = new FileInputStream ( fileName )))) { BufferedReader reader = new BufferedReader ( new InputStreamReader ( stream )); // Usar reader. } // stream se cierra cuando se sale del bloque try-with-resources. // reader ya no se puede usar después de que stream se cierra, pero siempre que no escape del bloque, esto no es un problema.

Por el contrario, en Python, un csv.reader no es propietario del fileque está leyendo, por lo que no hay necesidad (ni es posible) de cerrar el lector, y en su lugar filese debe cerrar el propio csv. [ 8 ]

importar csv desde typing importar iteradorcon open ( filename ) como f : r : Iterator [ list [ str ]] = csv . reader ( f ) # Usar r. # f se cierra cuando se sale de la instrucción with y ya no se puede usar. # No se hace nada con r, pero el f subyacente se cierra, por lo que r tampoco se puede usar.

En .NET , la convención es que solo el usuario directo de los recursos sea responsable: "Solo debe implementar IDisposable si su tipo utiliza recursos no administrados directamente". [ 9 ]

En caso de un grafo de objetos más complejo , como múltiples objetos que comparten un recurso, o ciclos entre objetos que contienen recursos, la gestión adecuada de los recursos puede ser bastante compleja, y surgen exactamente los mismos problemas que en la finalización de objetos (mediante destructores o finalizadores); por ejemplo, puede ocurrir el problema del oyente interrumpido y causar fugas de recursos si se utiliza el patrón observador (y los observadores contienen recursos). Existen varios mecanismos para permitir un mayor control de la gestión de recursos. Por ejemplo, en la biblioteca Google Closure , la goog.Disposableclase proporciona un registerDisposablemétodo para registrar otros objetos que se eliminarán junto con este objeto, junto con varios métodos de instancia y de clase de nivel inferior para gestionar la eliminación.

Programación estructurada

En la programación estructurada , la gestión de recursos de la pila se realiza simplemente anidando el código lo suficiente para manejar todos los casos. Esto requiere solo un único retorno al final del código y puede resultar en un código muy anidado si se deben adquirir muchos recursos, lo que algunos consideran un antipatrón : el antipatrón de la flecha [ 10 ], debido a la forma triangular que adquiere el anidamiento sucesivo.

Cláusula de limpieza

Otro enfoque, que permite una salida temprana pero centraliza la limpieza en un solo lugar, consiste en tener una única instrucción `out` de la función, precedida por el código de limpieza, y usar `goto` para saltar a la limpieza antes de la salida. Esto se ve con poca frecuencia en el código moderno, pero ocurre en algunos usos de C.

Véase también

Referencias

  1. 1 2 Beck 1997 , págs. 37–39.
  2. 1 2 Elder, Jackson y Liblit 2008 , pág. 3.
  3. Elder, Jackson y Liblit 2008 , pág. 2.
  4. ^ " Genérico: cambie la forma de escribir código seguro para excepciones, para siempre ", por Andrei Alexandrescu y Petru Marginean, 1 de diciembre de 2000, Dr. Dobb's
  5. ScopeGuard 2.0 , Joshua Lehrer
  6. D: Seguridad ante excepciones
  7. Aplazar, entrar en pánico y recuperarse , Andrew Gerrand, The Go Blog, 4 de agosto de 2010
  8. Python: ¿No se puede cerrar el archivo csv()?
  9. "Interfaz IDisposable" . Consultado el 3 de abril de 2016 .
  10. Código de flecha aplanadora , Jeff Atwood, 10 de enero de 2006
  • Beck, Kent (1997). Patrones de mejores prácticas de Smalltalk . Prentice Hall. ISBN 978-0134769042.
  • Elder, Matt; Jackson, Steve; Liblit, Ben (octubre de 2008). Code Sandwiches (PDF) (Informe técnico). Universidad de Wisconsin-Madison . 1647, resumen{{cite tech report}}: Enlace externo en |postscript=( ayuda ) CS1 maint: postscript ( enlace )

Lecturas adicionales

  • Actualización de DG: Eliminación, finalización y gestión de recursos , Joe Duffy