En programación de computadoras , la administración de recursos se refiere a técnicas para administrar recursos (componentes con disponibilidad limitada).
Los programas informáticos pueden gestionar sus propios recursos [ ¿cuáles? ] mediante el uso de características expuestas por lenguajes de programación ( Elder, Jackson & Liblit (2008) es un artículo de encuesta que contrasta diferentes enfoques), o puede optar por administrarlos mediante un host (un sistema operativo o máquina virtual ) u otro programa.
La administración basada en host se conoce como seguimiento de recursos y consiste en limpiar las fugas de recursos: terminar el acceso a los recursos que se han adquirido pero no 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 memoria. En muchos sistemas, el sistema operativo recupera recursos después de que el proceso realiza la llamada al sistema de salida .
Controlando el acceso
La omisión de liberar un recurso cuando un programa ha terminado de usarlo se conoce como una fuga de recursos y es un problema en la computación secuencial. Múltiples procesos que desean acceder a un recurso limitado pueden 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 garantizar que un recurso se libera si y solo si se adquiere con éxito. Este problema general se puede resumir como código " antes, cuerpo y después ", que normalmente se ejecutan en este orden, con la condición de que se llame al código posterior si y solo si el código anterior se completa correctamente, independientemente de si el código del cuerpo se ejecuta con éxito o no. Esto también se conoce como ejecutar alrededor de [1] o un sándwich de código, y ocurre en varios otros contextos, [2] como un cambio temporal del estado del programa o el rastreo de la entrada y salida de una subrutina . Sin embargo, la gestión de recursos es la aplicación más citada. En la programación orientada a aspectos , dicha ejecución en torno a la lógica es una forma de consejo .
En la terminología del análisis de flujo de control , la liberación de recursos debe ser posterior a la adquisición exitosa de recursos; [3] falla al asegurar que esto es un error, y una ruta de código que viola esta condición causa una fuga de recursos. Las fugas de recursos son a menudo problemas menores, que generalmente no bloquean el programa, sino que causan una ralentización del programa o del sistema en general. [2] Sin embargo, pueden causar bloqueos, ya sea en el programa mismo o en otros programas, debido al agotamiento de los recursos: si el sistema se queda sin recursos, las solicitudes de adquisición fallan. Esto puede presentar un error de seguridad si un ataque puede provocar el agotamiento de los recursos. Las fugas de recursos pueden ocurrir bajo el flujo regular del programa, como simplemente olvidarse de 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 una salida anticipada de una subrutina, ya sea por una return
declaración o una excepción generada por la subrutina misma o una subrutina más profunda a la que llama. Si bien la liberación de recursos debido a declaraciones de devolución se puede manejar liberando cuidadosamente dentro de la subrutina antes de la devolución, las excepciones no se pueden manejar sin alguna facilidad de lenguaje adicional que garantice que se ejecuta el código de liberación.
De manera 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 un lanzamiento tan incorrecto van desde ser ignorado en silencio hasta bloquear el programa o un comportamiento impredecible. Estos errores generalmente se manifiestan raramente, ya que requieren la asignación de recursos para fallar primero, lo que generalmente es un caso excepcional. Además, es posible que las consecuencias no sean graves, ya que el programa ya puede fallar debido a la imposibilidad de adquirir un recurso esencial. Sin embargo, estos pueden evitar la recuperación de la falla o convertir un apagado ordenado en un apagado desordenado. Esta condición generalmente se asegura al verificar primero que el recurso se adquirió con éxito antes de liberarlo, ya sea al tener una variable booleana para registrar "adquirido con éxito", que carece de atomicidad si el recurso se adquiere pero la variable de marca no se actualiza, o viceversa - o por el identificador del recurso que es un tipo anulable , donde "nulo" indica "no adquirido con éxito", lo que garantiza la atomicidad.
Contención de recursos
Gestión de la memoria
La memoria se puede tratar como un recurso, pero la administración de la memoria generalmente se considera por separado, principalmente porque la asignación y desasignación de memoria es significativamente más frecuente que la adquisición y liberación de otros recursos, como los identificadores de archivos. La memoria administrada por un sistema externo tiene similitudes tanto con la administración de memoria (interna) (ya que es memoria) como con la administración de recursos (ya que es administrada por un sistema externo). Los ejemplos incluyen memoria administrada a través de código nativo y utilizada desde Java (a través de la interfaz nativa de Java ); y objetos en el Modelo de objetos de documento (DOM), utilizado desde JavaScript . En ambos casos, el administrador de memoria ( recolector de basura ) del entorno de ejecución (máquina virtual) no puede administrar la memoria externa (no hay administración de memoria compartida) y, por lo tanto, la memoria externa se trata como un recurso y se administra de manera análoga. . Sin embargo, los ciclos entre sistemas (JavaScript se refiere al DOM, refiriéndose a JavaScript) pueden dificultar o imposibilitar la gestión.
Gestión léxica y gestión explícita
Una distinción clave en la administración de recursos dentro de un programa es entre administración léxica y administración explícita : si un recurso puede manejarse como si tuviera un alcance léxico, como una variable de pila (la vida útil está restringida a un alcance léxico único, que se adquiere al ingresar ao dentro de un alcance particular, y liberado cuando la ejecución sale de ese alcance), o si un recurso debe asignarse y liberarse explícitamente, como un recurso adquirido dentro de una función y luego devuelto de ella, que luego debe liberarse fuera de la función adquirente. La gestión léxica, cuando corresponde, permite una mejor separación de preocupaciones y es menos propensa a errores.
Tecnicas basicas
El enfoque básico para la gestión de recursos es adquirir un recurso, hacer algo con él y luego liberarlo, obteniendo el código del formulario (ilustrado con la apertura de un archivo en Python):
f = abrir ( nombre de archivo ) ... f . cerrar ()
Esto es correcto si el ...
código intermedio no contiene una salida anticipada ( return
), el lenguaje no tiene excepciones y open
se garantiza que tendrá éxito. Sin embargo, provoca una fuga de recursos si hay una devolución o una excepción, y provoca una liberación incorrecta de un recurso no adquirido si open
puede fallar.
Hay dos problemas fundamentales más: el par adquisición-liberación no es adyacente (el código de liberación debe escribirse lejos del código de adquisición) y la administración de recursos no está encapsulada; el programador debe asegurarse manualmente de que siempre estén emparejados. En combinación, esto significa que la adquisición y la liberación deben emparejarse explícitamente, pero no pueden colocarse juntas, lo que facilita que no se emparejen correctamente.
La fuga de recursos se puede resolver en lenguajes que admiten una finally
construcción (como Python) colocando el cuerpo en una try
cláusula y el lanzamiento en una finally
cláusula:
f = abrir ( nombre de archivo ) intente : ... finalmente : f . cerrar ()
Esto asegura la 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 se produce antes de la try
cláusula, lo que garantiza que la finally
cláusula solo se ejecute si el open
código tiene éxito (sin generar una excepción), asumiendo que "sin excepción" significa "éxito" (como es el caso de open
Python). Si la adquisición de recursos puede fallar sin generar una excepción, como devolver una forma de null
, también se debe verificar antes del lanzamiento, como por ejemplo:
f = abrir ( nombre de archivo ) intente : ... 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 with
declaració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 encuentran en varias formas en C #, Common Lisp , Java, Python, Ruby, Scheme y Smalltalk , [1] entre otros; datan de finales de la década de 1970 en el dialecto NIL de Lisp; consulte Manejo de excepciones § Historial . Hay muchas variaciones en la implementación y también hay enfoques significativamente diferentes .
Enfoques
Relájese protección
El enfoque más común para la administración de recursos en todos los lenguajes es usar la protección de desenrollado, que se llama cuando la ejecución sale de un alcance, al ejecutar la ejecución desde el final del bloque, regresar desde dentro del bloque o lanzar una excepción. Esto funciona para recursos administrados por pila y se implementa en muchos lenguajes, incluidos C #, Common Lisp, Java, Python, Ruby y Scheme. El principal problema con este enfoque es que el código de liberación (más comúnmente en una finally
cláusula) puede estar muy distante del código de adquisición (carece de adyacencia ), y que el código de adquisición y liberación siempre debe ser emparejado por la persona que llama (carece de encapsulación ). Estos pueden remediarse funcionalmente, usando cierres / devoluciones de llamada / corrutinas (Common Lisp, Ruby, Scheme), o usando un objeto que maneje tanto la adquisición como la liberación, y agregando una construcción de lenguaje para llamar a estos métodos cuando el control entra y sale un alcance (C # using
, Java try
-con-recursos, Python with
); vea abajo.
Un enfoque alternativo, más imperativo, es escribir código asincrónico 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 alcance: adquisición sincrónica seguida de liberación asincrónica. 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 de lenguaje directo en D a través de la scope
palabra clave ( ScopeGuardStatement ), donde es un enfoque seguridad de excepción , además de RAII (ver abajo). [6] También se ha incluido en Go, como defer
declaració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 cuanto al código, evite 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 un file
objeto que tiene un campo cuyo valor es un descriptor de archivo (o un identificador de archivo más general ). Esto permite que el objeto utilice y gestione el recurso sin que los usuarios del objeto tengan que hacerlo. Sin embargo, existe una amplia variedad de formas en las que los objetos y los recursos pueden relacionarse.
En primer lugar, está la cuestión de la propiedad: ¿un objeto tiene un recurso?
- Los objetos pueden poseer recursos (a través de la composición de objetos , una fuerte relación "tiene una").
- Los objetos pueden ver recursos (a través de la agregación de objetos , una relación débil "tiene").
- Los objetos pueden comunicarse con otros objetos que tienen recursos (a través de Asociación ).
Los objetos que tienen un recurso pueden adquirirlo y liberarlo de diferentes maneras, en diferentes puntos durante la vida útil del objeto ; estos ocurren en pares, pero en la práctica a menudo no se usan simétricamente (ver más abajo):
- Adquirir / liberar mientras el objeto es válido, a través de métodos (instancia) como
open
odispose
. - Adquirir / liberar durante la creación / destrucción de objetos (en el inicializador y finalizador).
- No adquirir ni liberar el recurso, sino simplemente tener una vista o referencia a un recurso administrado externamente al objeto, como en la inyección de dependencia ; 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 común es adquirir un recurso durante la creación del objeto y luego liberarlo explícitamente a través de un método de instancia, comúnmente llamado dispose
. Esto es análogo a la gestión de archivos tradicional (adquirir durante open
, liberar por explícito close
) y se conoce como patrón de eliminación . Este es el enfoque básico utilizado en varios de los principales lenguajes orientados a objetos modernos, incluidos Java , C # y Python , y estos lenguajes tienen construcciones adicionales para automatizar la gestión de recursos. Sin embargo, incluso en estos lenguajes, las relaciones de objetos más complejas dan como resultado una gestión de recursos más compleja, como se analiza a continuación.
RAII
Un enfoque natural es hacer que la posesión de un recurso sea una clase invariante : 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 es inicialización (RAII) y vincula la administración de recursos con la vida útil del objeto , lo que garantiza que los objetos activos tengan todos los recursos necesarios. Otros enfoques no hacen que mantener el recurso sea una clase invariante y, por lo tanto, es posible que los objetos no tengan los recursos necesarios (porque aún no se han adquirido, ya se han liberado o se están administrando externamente), lo que genera errores como intentar leer de un archivo cerrado. Este enfoque vincula la gestión de recursos con la gestión de la memoria (específicamente la gestión de objetos), por lo que si no hay fugas de memoria (no hay 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 recursos gestionados por la pila, y es componible: los recursos mantenidos por objetos en relaciones arbitrariamente complicadas (un gráfico de objeto complicado ) se liberan de forma transparente simplemente mediante la destrucción de objetos (¡siempre que esto se haga correctamente! ).
RAII es el enfoque de administración de recursos estándar en C ++, pero se usa poco fuera de C ++, a pesar de su atractivo, porque funciona mal con la administración automática de memoria moderna, específicamente rastreando la recolección de basura : RAII vincula la administración de recursos a la administración de memoria, pero estos tienen diferencias significativas . En primer lugar, debido a que los recursos son costosos, es deseable liberarlos rápidamente, por lo que los objetos que contienen recursos deben destruirse tan pronto como se conviertan en basura (ya no se utilicen). La destrucción de objetos es rápida en la gestión de memoria determinista, como en C ++ (los objetos asignados a la pila se destruyen en el desenrollado de la pila, los objetos asignados al montón se destruyen manualmente mediante una llamada delete
o el uso automático unique_ptr
) o en el recuento de referencias determinista (donde los objetos se destruyen inmediatamente cuando su recuento de referencias cae a 0), por lo que RAII funciona bien en estas situaciones. Sin embargo, la gestión de memoria automática más moderna no es determinista, por lo que no garantiza que los objetos se destruyan rápidamente o incluso que se destruyan en absoluto. Esto se debe a que es más barato dejar un poco 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 objetos significa que un objeto debe tener un finalizador (en la gestión de memoria determinista conocida como destructor ), el objeto no puede simplemente desasignarse, lo que complica y ralentiza significativamente la recolección de basura.
Relaciones complejas
Cuando varios objetos dependen de un solo recurso, la gestión de recursos puede resultar complicada.
Una cuestión fundamental es si una relación "tiene una" es la de poseer otro objeto ( composición de objetos ) o ver otro objeto ( agregación de objetos ). Un caso común es cuando se encadenan dos objetos, como en el patrón de tubería y filtro , el patrón de delegación , el patrón de decorador o el patrón de adaptador . Si el segundo objeto (que no se usa directamente) contiene un recurso, ¿es el primer objeto (que se usa directamente) responsable de administrar el recurso? Esto generalmente se responde de manera idéntica a si el primer objeto posee el segundo objeto: si es así, entonces el objeto propietario también es responsable de la gestión de recursos ("tener un recurso" es transitivo ), mientras que si no es así, entonces no lo es. Además, un solo objeto puede "tener" varios otros objetos, poseer algunos y ver otros.
Ambos casos se encuentran comúnmente y las convenciones difieren. Hacer que los objetos que usan recursos sean responsables indirectamente del recurso (composición) proporciona encapsulación (solo se necesita el objeto que usan los clientes, sin objetos separados para los recursos), pero resulta en una complejidad considerable, particularmente cuando un recurso es compartido por múltiples objetos o los objetos tienen relaciones complejas. Si solo el objeto que usa directamente el recurso es responsable del recurso (agregación), las relaciones entre otros objetos que usan los recursos pueden ignorarse, pero no hay encapsulación (más allá del objeto que usa directamente): el recurso debe administrarse directamente, y puede que no esté disponible para el objeto que lo usa indirectamente (si se ha liberado por separado).
En cuanto a la implementación, en la composición de objetos, si se usa el patrón de disposición, el objeto propietario también tendrá un dispose
método, que a su vez llama a los dispose
métodos de los objetos propios que deben eliminarse; en RAII esto se maneja automáticamente (siempre que los objetos propios se destruyan automáticamente: en C ++ si son un valor o un unique_ptr
, pero no un puntero en bruto: ver propiedad del puntero ). En la agregación de objetos, el objeto de visualización no debe hacer nada, ya que no es responsable del recurso.
Ambos se encuentran comúnmente. Por ejemplo, en la biblioteca de clases de Java , Reader#close()
cierra el flujo subyacente y estos se pueden encadenar. Por ejemplo, a BufferedReader
puede contener a InputStreamReader
, que a su vez contiene a FileInputStream
, y al llamar close
a, BufferedReader
a su vez se cierra el InputStreamReader
, que a su vez cierra el FileInputStream
, que a su vez libera el recurso de archivo del sistema. De hecho, el objeto que utiliza directamente el recurso puede incluso ser anónimo, gracias a la encapsulación:
try ( BufferedReader reader = new BufferedReader ( new InputStreamReader ( new FileInputStream ( fileName )))) { // Usar 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 administrar solo el objeto que usa directamente el recurso y no usar la administración de recursos en los objetos de envoltura:
try ( FileInputStream stream = new FileInputStream ( fileName )))) { Lector BufferedReader = nuevo BufferedReader ( nuevo InputStreamReader ( stream )); // Usar lector. } // la secuencia se cierra cuando se sale del bloque try-with-resources. // El lector ya no se puede usar después de cerrar la secuencia, pero mientras no escape del bloque, esto no es un problema.
Por el contrario, en Python, un csv.reader no es propietario de lo file
que está leyendo, por lo que no es necesario (y no es posible) cerrar el lector, sino que file
debe cerrarse el propio lector . [8]
con open ( nombre de archivo ) como f : r = csv . lector ( f ) # Utilice r. # f se cierra cuando se sale de la instrucción with y ya no se puede usar. # No se hace nada para r, pero la f subyacente está cerrada, por lo que tampoco se puede usar r.
En .NET , la convención es que solo el usuario directo de los recursos sea responsable: "Debe implementar IDisposable solo si su tipo usa recursos no administrados directamente". [9]
En el caso de un gráfico de objetos más complicado , como varios objetos que comparten un recurso, o ciclos entre objetos que contienen recursos, la gestión adecuada de los recursos puede ser bastante complicada y surgen exactamente los mismos problemas que en la finalización de objetos (a través de destructores o finalizadores); por ejemplo, el problema del oyente inactivo puede ocurrir y causar pérdidas de recursos si se usa el patrón de observador (y los observadores tienen recursos). Existen varios mecanismos para permitir un mayor control de la gestión de recursos. Por ejemplo, en la biblioteca de cierre de Google , la goog.Disposable
clase proporciona un registerDisposable
método para registrar otros objetos que se eliminarán con este objeto, junto con varios métodos de clase e instancia de nivel inferior para administrar 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 retorno al final del código, y puede resultar en un código fuertemente anidado si se deben adquirir muchos recursos, lo cual es considerado un anti-patrón por algunos - Arrow Anti Pattern , [10] debido a la forma triangular de los sucesivos anidamientos.
Cláusula de limpieza
Otro enfoque, que permite un retorno temprano pero consolida la limpieza en un solo lugar, es tener un retorno de salida único de una función, precedido por un código de limpieza, y usar goto para saltar a la limpieza antes de salir. Esto se ve con poca frecuencia en el código moderno, pero ocurre en algunos usos de C.
Ver también
- Gestión de la memoria
- Piscina (informática)
Referencias
- ↑ a b Beck , 1997 , págs. 37–39.
- ↑ a b Elder, Jackson y Liblit , 2008 , p. 3.
- ^ Elder, Jackson y Liblit 2008 , p. 2.
- ^ " Genérico: cambie la forma en que escribe el código seguro para excepciones - para siempre ", por Andrei Alexandrescu y Petru Marginean, 01 de diciembre de 2000, Dr. Dobb
- ^ ScopeGuard 2.0 , Joshua Lehrer
- ^ D: Seguridad de excepción
- ^ Aplazar, entrar en pánico y recuperar , Andrew Gerrand, The Go Blog, 4 de agosto de 2010
- ^ Python: ¿Sin csv.close ()?
- ^ "Interfaz IDisposable" . Consultado el 3 de abril de 2016 .
- ^ Código de flecha de aplanamiento , Jeff Atwood, 10 de enero de 2006
- Beck, Kent (1997). Patrones de mejores prácticas de Smalltalk . Prentice Hall. ISBN 978-0134769042.
- Anciano, Matt; Jackson, Steve; Liblit, Ben (octubre de 2008). Code Sandwiches (PDF) (Informe técnico). Universidad de Wisconsin – Madison . 1647, abstractoCS1 maint: posdata ( enlace )
Otras lecturas
- Actualización de DG: Eliminación, finalización y gestión de recursos , Joe Duffy
enlaces externos
- Gestión de recursos determinista , WikiWikiWeb