En programación informática , la gestión de recursos se refiere a técnicas para gestionar recursos (componentes con disponibilidad limitada).
Los programas de computadora pueden administrar sus propios recursos [ ¿cuáles? ] mediante el uso de características expuestas por los lenguajes de programación (Elder, Jackson y Liblit (2008) es un artículo de investigación que contrasta diferentes enfoques), o pueden elegir administrarlos mediante un host (un sistema operativo o una máquina virtual ) u otro programa.
La gestión basada en host se conoce como seguimiento de recursos y consiste en limpiar las fugas de recursos: poner fin al 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 recursos después de que el proceso realiza la llamada al sistema de salida .
La omisión de liberar un recurso cuando un programa ha terminado de usarlo se conoce como pérdida 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 evitar ambas situaciones.
Formalmente, la gestión de recursos (prevenir 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 ejecutan en este orden, con la condición de que el código después se llame si y solo si el código anterior se completa con éxito, independientemente de si el código del cuerpo se ejecuta con éxito o no. Esto también se conoce como ejecutar alrededor [1] o sándwich de código, y ocurre en varios otros contextos, [2] como un cambio temporal del estado del programa o el seguimiento de la entrada y salida en una subrutina . Sin embargo, la gestión de recursos es la aplicación más comúnmente 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 a menudo son problemas menores, que generalmente no bloquean el programa, sino que causan cierta desaceleración en el programa o en el sistema en general. [2] Sin embargo, pueden causar fallas, ya sea del programa en sí o de 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 causar el agotamiento de los recursos. Las fugas de recursos pueden ocurrir bajo el flujo regular 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 temprana de una subrutina, ya sea por una return
declaración o una excepción generada por la propia subrutina o una subrutina más profunda que llama. Si bien la liberación de recursos debido a las declaraciones de retorno se puede manejar mediante una liberación cuidadosa 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 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 una liberación incorrecta de este tipo van desde ser ignorada silenciosamente hasta bloquear el programa o un comportamiento impredecible. Estos errores generalmente se manifiestan raramente, ya que requieren que la asignación de recursos falle primero, lo que generalmente es un caso excepcional. Además, las consecuencias pueden no ser graves, ya que el programa ya puede estar fallando debido a la falla en la adquisición de 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 verificando primero que el recurso se adquirió correctamente antes de liberarlo, ya sea teniendo una variable booleana para registrar "adquirido exitosamente" - que carece de atomicidad si el recurso se adquiere pero la variable de bandera no se actualiza, o viceversa - o por el identificador del recurso que es un tipo nulo , donde "null" indica "no adquirido exitosamente", lo que garantiza la atomicidad.
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 memoria de acceso aleatorio, almacenamiento en disco, memoria caché, buses internos o dispositivos de red externos.
{{expandir sección| contención de recursos
La memoria puede ser tratada como un recurso, pero la administración de 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 manejadores 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 Java Native Interface ); y objetos en el Document Object Model (DOM), utilizados 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 haciendo referencia al DOM, haciendo referencia de nuevo a JavaScript) pueden dificultar o imposibilitar la administración.
Una distinción clave en la gestión de recursos dentro de un programa es entre la gestión léxica y la gestión explícita : si un recurso se puede manejar como si tuviera un alcance léxico, como una variable de pila (su duración está restringida a un único alcance léxico, se adquiere al ingresar a un alcance particular o dentro de él y se libera cuando la ejecución sale de ese alcance), o si un recurso se debe asignar y liberar explícitamente, como un recurso adquirido dentro de una función y luego devuelto desde ella, que luego se debe liberar fuera de la función que lo adquiere. La gestión léxica, cuando corresponde, permite una mejor separación de preocupaciones y es menos propensa a errores.
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 con el formato (ilustrado con la apertura de un archivo en Python):
f = abrir ( nombre del 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 pérdida de recursos si hay un retorno o una excepción, y provoca una liberación incorrecta de recursos no adquiridos si open
puede fallar.
Existen 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 gestión de recursos no está encapsulada: el programador debe asegurarse manualmente de que siempre estén emparejados. En conjunto, esto significa que la adquisición y la liberación deben emparejarse explícitamente, pero no pueden colocarse juntas, lo que hace que sea fácil 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 la liberación en otra finally
cláusula:
f = open ( nombre_archivo ) intentar : ... finalmente : f.close ( )
Esto garantiza 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 lanzar una excepción), suponiendo que "sin excepción" significa "éxito" (como es el caso de open
en Python). Si la adquisición de recursos puede fallar sin lanzar una excepción, como al devolver una forma de null
, también debe comprobarse antes de la liberación, como:
f = open ( nombre_archivo ) try : ... finally : if f : f.close ( )
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 abierto ( 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 fines de la década de 1970 en el dialecto NIL de Lisp; consulte Manejo de excepciones § Historia . Hay muchas variaciones en la implementación y también hay enfoques significativamente diferentes.
El enfoque más común para la gestión de recursos en todos los lenguajes es utilizar la protección de desenrollado, que se invoca cuando la ejecución sale de un ámbito (por ejemplo, si la ejecución se ejecuta al final del bloque, regresa desde dentro del bloque o se lanza 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. Los principales problemas con este enfoque son 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 deben estar emparejados por el llamador (carece de encapsulación ). Estos problemas se pueden remediar funcionalmente, mediante el uso de cierres/devoluciones de llamadas/corrutinas (Common Lisp, Ruby, Scheme), o mediante el uso de 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 ingresa y sale de un ámbito (C# using
, Java try
-with-resources, Python with
); consulte a continuación.
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 ámbito: 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 directo del lenguaje en D a través de la scope
palabra 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 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).
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 use y administre 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: ¿ tiene un objeto un recurso?
Los objetos que tienen un recurso pueden adquirirlo y liberarlo de diferentes maneras, en diferentes puntos durante la vida útil del objeto ; esto ocurre en pares, pero en la práctica a menudo no se utilizan simétricamente (ver a continuación):
open
o dispose
.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 mediante explícito close
), y se conoce como el patrón de disposició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.
Un enfoque natural es hacer que la retenció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 la retención del recurso sea una clase invariante y, por lo tanto, los objetos pueden no tener los recursos necesarios (porque aún no se han adquirido, ya se han liberado o se están administrando externamente), lo que resulta en errores como intentar leer desde un archivo cerrado. Este enfoque vincula la administración de recursos con la administración de memoria (específicamente la administració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 manera natural para recursos administrados por montón, no solo recursos administrados por pila, y es componible: los recursos mantenidos por objetos en relaciones arbitrariamente complicadas (un gráfico de objetos complicado ) se liberan de manera transparente simplemente por la destrucción del objeto (¡siempre que esto se haga correctamente!).
RAII es el enfoque estándar de administración de recursos en C++, pero se usa poco fuera de C++, a pesar de su atractivo, porque funciona mal con la administración de memoria automática moderna, específicamente el seguimiento de la recolección de basura : RAII vincula la administración de recursos con la administración de memoria, pero estas tienen diferencias significativas. En primer lugar, debido a que los recursos son caros, 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 rápida en la administració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 automáticamente usando unique_ptr
) o en el conteo de referencias determinista (donde los objetos se destruyen inmediatamente cuando su conteo de referencias cae a 0), y por lo tanto RAII funciona bien en estas situaciones. Sin embargo, la mayoría de la administración de memoria automática moderna no es determinista, ¡no ofrece garantías de que los objetos se destruyan 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 objetos significa que un objeto debe tener un finalizador (en la gestión de memoria determinista, conocido como destructor ): el objeto no puede simplemente desasignarse, lo que complica y ralentiza significativamente la recolección de basura.
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" es una de posesión de otro objeto ( composición de objetos ) o de visualización de otro objeto ( agregación de objetos ). Un caso común es cuando uno o dos objetos están encadenados, 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 utiliza directamente) contiene un recurso, ¿es el primer objeto (que se utiliza directamente) responsable de gestionar el recurso? Esto generalmente se responde de forma 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, entonces no lo es. Además, un solo objeto puede "tener" varios otros objetos, poseer algunos y ver otros.
Ambos casos son comunes y las convenciones difieren. El hecho de que los objetos que utilizan recursos indirectamente sean responsables del recurso (composición) proporciona encapsulación (solo se necesita el objeto que utilizan los clientes, sin objetos separados para los recursos), pero genera una complejidad considerable, en particular cuando un recurso es compartido por varios objetos o los objetos tienen relaciones complejas. Si solo el objeto que utiliza directamente el recurso es responsable del recurso (agregación), se pueden ignorar las relaciones entre otros objetos que utilizan los recursos, pero no hay encapsulación (más allá del objeto que utiliza directamente): el recurso debe administrarse directamente y es posible que no esté disponible para el objeto que lo utiliza indirectamente (si se ha publicado 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 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 sin formato: consulte propiedad del puntero). En la agregación de objetos, el objeto de visualización 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 pueden encadenarse. Por ejemplo, a BufferedReader
puede contener a InputStreamReader
, que a su vez contiene a FileInputStream
, y al llamar close
a , BufferedReader
cierra a su vez InputStreamReader
, que a su vez cierra FileInputStream
, que a su vez libera el recurso de 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, lo que cierra cada uno de los objetos contenidos en secuencia.
Sin embargo, también es posible administrar solo el objeto que utiliza directamente el recurso y no utilizar la administración de recursos en objetos envolventes:
try ( FileInputStream stream = new FileInputStream ( fileName )))) { BufferedReader reader = new BufferedReader ( new InputStreamReader ( stream )); // Usar el lector. } // El flujo se cierra cuando se sale del bloque try-with-resources. // El lector ya no se puede usar después de que se cierra el flujo, pero siempre que no escape del bloque, esto no es un problema.
Por el contrario, en Python, un csv.reader no es propietario del file
objeto que está leyendo, por lo que no hay necesidad (y no es posible) de cerrar el lector, y en su lugar el file
objeto mismo debe estar cerrado. [8]
con open ( filename ) como f : r = csv . reader ( f ) # Use 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 se cierra la f subyacente, por lo que tampoco se puede usar r.
En .NET , la convención es que solo los usuarios directos de los recursos sean responsables: "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 múltiples objetos que comparten un recurso o ciclos entre objetos que contienen recursos, la administració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, puede ocurrir el problema del oyente caducado y causar fugas de recursos si se usa el patrón de observador (y los observadores contienen recursos). Existen varios mecanismos para permitir un mayor control de la administración de recursos. Por ejemplo, en Google Closure Library , 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 instancia y clase de nivel inferior para administrar la eliminación.
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 dar como resultado un código muy anidado si se deben adquirir muchos recursos, lo que algunos consideran un antipatrón : el antipatrón de flecha [10] , debido a la forma triangular del anidamiento sucesivo.
Otro enfoque, que permite un retorno temprano pero consolida la limpieza en un solo lugar, es tener un único retorno de salida de una función, precedido por el 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.
{{cite tech report}}
: Enlace externo en |postscript=
( ayuda )Mantenimiento de CS1: posdata ( enlace )