stringtranslate.com

Gestión de recursos (informática)

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

Los programas informáticos pueden gestionar sus propios recursos [ ¿cuáles? ] mediante el uso de características expuestas por los lenguajes de programación (Elder, Jackson & Liblit (2008) es un artículo de encuesta que contrasta diferentes enfoques), o puede optar por administrarlas mediante un host (un sistema operativo o 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: finalizar el acceso a los recursos que han sido adquiridos pero no liberados 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 de salida del sistema .

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. 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 libere si y sólo 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 sólo si el código anterior se completa con éxito, independientemente de si el código del cuerpo se ejecuta exitosamente o no. Esto también se conoce como ejecutar alrededor de [1] o un código sándwich, y ocurre en varios otros contextos, [2] como un cambio temporal del estado del programa o el seguimiento 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 del flujo de control , la liberación de recursos debe prevalecer después de la adquisición exitosa de recursos; [3] no garantizar que esto sea un error, y una ruta de código que viole esta condición provoca una fuga de recursos. Las fugas de recursos suelen ser problemas menores que, por lo general, no bloquean el programa, sino que causan cierta desaceleración en el programa o en el sistema en general. [2] Sin embargo, pueden provocar fallos (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 provocar el agotamiento de los recursos. Las fugas de recursos pueden ocurrir durante el flujo normal 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 la salida anticipada de una subrutina, ya sea por una returndeclaración o 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 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 función de lenguaje adicional que garantice que se ejecute el código de liberación.

Más sutilmente, 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 publicación tan incorrecta van desde ser ignorado 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, es posible que las consecuencias no sean graves, ya que es posible que el programa ya esté fallando debido a que no se pudo adquirir un recurso esencial. Sin embargo, estos pueden impedir la recuperación de la falla o convertir un apagado ordenado en un apagado desordenado. Esta condición generalmente se garantiza verificando primero que el recurso se adquirió exitosamente antes de liberarlo, ya sea teniendo una variable booleana para registrar "adquirido exitosamente", lo cual carece de atomicidad si el recurso se adquiere pero la variable de bandera no se actualiza, o a la inversa. – o porque el identificador del recurso es de 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 está administrada por un sistema externo). Los ejemplos incluyen memoria administrada mediante 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), 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 se refiere al DOM, se refiere nuevamente a JavaScript) pueden dificultar o imposibilitar la administración.

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

Una distinción clave en la gestión de recursos dentro de un programa es entre gestión léxica y gestió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 único alcance léxico, que se adquiere al ingresar o 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 desde ella, que luego debe liberarse fuera de la función de adquisición. La gestión léxica, cuando corresponde, permite una mejor separación de preocupaciones 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 al abrir un archivo en Python):

f  =  abrir ( nombre de archivo ) ... f . cerca ()

Esto es correcto si el ...código intermedio no contiene una salida anticipada ( return), el lenguaje no tiene excepciones y opense garantiza que tendrá éxito. Sin embargo, provoca una pérdida de recursos si hay una devolución o una excepción y provoca una liberación incorrecta de recursos no adquiridos si openpuede 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 gestió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 admitan una finallyconstrucción (como Python) colocando el cuerpo en una trycláusula y la liberación en una finallycláusula:

f  =  abrir ( nombre de archivo ) intentar :  ... finalmente :  f . cerca ()

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 generar 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 generar una excepción, como devolver un formulario de null, también se debe verificar antes del lanzamiento, como por ejemplo:

f  =  abrir ( nombre de archivo ) intente :  ... finalmente :  si  f :  f . cerca ()

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 withdeclaració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, y 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 enfoques significativamente diferentes.

Enfoques

Protección para relajarse

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 llama cuando la ejecución sale de un alcance: ejecutándose desde el final del bloque, regresando desde dentro del bloque o lanzando 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 finallycláusula) puede estar muy distante del código de adquisición (carece de adyacencia ), y que la persona que llama siempre debe emparejar el código de adquisición y de liberación (carece de encapsulación ). ). Estos se pueden remediar funcionalmente, usando cierres/devoluciones de llamada/rutinas (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 trycon 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, realizar 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 directo de lenguaje en D a través de la scopepalabra clave (ScopeGuardStatement), donde es un enfoque para excepción de seguridad , además de RAII (ver más abajo). [6] También se ha incluido en Go, como se indica en el defercomunicado. [7] Este enfoque carece de encapsulación (se 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 objetos que los utilizan, como un fileobjeto 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 necesiten hacerlo. Sin embargo, existe una amplia variedad de formas en que se pueden relacionar objetos y recursos.

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 ; Estos ocurren en pares, pero en la práctica a menudo no se usan simétricamente (ver más abajo):

Lo más común es adquirir un recurso durante la creación de un objeto y luego liberarlo explícitamente mediante un método de instancia, comúnmente llamado dispose. Esto es análogo a la gestión de archivos tradicional (adquirir durante open, liberar de forma explícita close) y se conoce como patrón de eliminación . Este es el enfoque básico utilizado en varios de los principales lenguajes modernos orientados a objetos, 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 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 gestión de recursos con la vida útil del objeto , asegurando 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 expediente 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 pérdidas de memoria (no hay pérdidas de objetos), no hay pérdidas de recursos . RAII funciona naturalmente para recursos administrados en montón, no solo recursos administrados en pila, y es componible: los recursos retenidos por objetos en relaciones arbitrariamente complicadas (un gráfico de objetos complicado ) se liberan de forma transparente simplemente mediante la destrucción de objetos (siempre que esto se haga correctamente). ).

RAII es el enfoque de gestión de recursos estándar en C++, pero se utiliza poco fuera de C++, a pesar de su atractivo, porque no funciona bien con la gestión de memoria automática moderna, específicamente con la recolección de basura de seguimiento : RAII vincula la gestión de recursos con la gestión de memoria, pero 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 estén en uso). 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 al desenrollarse la pila, los objetos asignados al montón se destruyen manualmente mediante llamadas deleteo automáticamente usando unique_ptr) o en el recuento de referencias determinista (donde los objetos se destruyen inmediatamente cuando su recuento de referencia 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 y no ofrece garantías de que los objetos se destruirán rápidamente o incluso que se destruirán en absoluto. Esto se debe a que es más barato dejar parte de la basura asignada que recolectar con precisión cada objeto inmediatamente después de que se convierta 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 determinista de la memoria conocido 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 ú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 ver otro objeto ( agregación de objetos ). Un caso común es cuando dos objetos están encadenados, como en el patrón de tubería y filtro , el patrón de delegación , el patrón decorador o el patrón 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? Por lo general, esto se responde de manera idéntica a si el primer objeto es propietario del 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 único objeto puede "tener" varios otros objetos, poseer algunos y visualizar otros.

Ambos casos son comunes y las convenciones difieren. Hacer que los objetos que usan recursos sean indirectamente responsables 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 se pueden ignorar, pero no hay encapsulación (más allá del objeto que usa 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 de eliminación, el objeto propietario también tendrá un disposemétodo, que a su vez llama a los disposemétodos de los objetos propios que deben eliminarse; en RAII esto se maneja automáticamente (siempre que los objetos de propiedad se destruyan automáticamente: en C++ si son un valor o a 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 se encuentran comúnmente. Por ejemplo, en la biblioteca de clases de Java , Reader#close()se cierra la secuencia subyacente y se pueden encadenar. Por ejemplo, a BufferedReaderpuede contener a InputStreamReader, que a su vez contiene a FileInputStream, y llamar closea BufferedReadera su vez cierra el InputStreamReader, que a su vez cierra el FileInputStream, lo que a su vez libera el recurso del archivo del sistema. De hecho, el objeto que utiliza directamente el recurso puede incluso ser anónimo gracias a la encapsulación:

try ( lector BufferedReader = nuevo BufferedReader ( nuevo InputStreamReader ( nuevo FileInputStream ( nombre de archivo )))) { // 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 objetos contenedores:

intente ( corriente FileInputStream = nuevo FileInputStream ( nombre de archivo )))) { lector BufferedReader = nuevo BufferedReader ( nuevo InputStreamReader ( corriente )); // Usa lector. } // la transmisión se cierra cuando se sale del bloque try-with-resources. // el lector ya no se puede utilizar después de cerrar la transmisión, pero mientras no escape del bloque, esto no es un problema.             

Por el contrario, en Python, un csv.reader no posee lo fileque está leyendo, por lo que no es necesario (y no es posible) cerrar el lector, sino que filese debe cerrar 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 declaración with y ya no se puede utilizar. # No se hace nada con r, pero la f subyacente está cerrada, por lo que r tampoco se puede utilizar.

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 (mediante destructores o finalizadores); por ejemplo, el problema del oyente caducado puede ocurrir y causar fugas de recursos si se usa el patrón de observador (y los observadores retienen 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.Disposableclase proporciona un registerDisposablemé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 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 suficiente para manejar todos los casos. Esto requiere solo un 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 flecha, [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 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.

Ver también

Referencias

  1. ^ ab Beck 1997, págs. 37–39.
  2. ^ ab Elder, Jackson y Liblit 2008, pág. 3.
  3. ^ Elder, Jackson y Liblit 2008, pág. 2.
  4. ^ "Genérico: cambie la forma en que escribe 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 de excepción
  7. ^ Aplazar, entrar en pánico y recuperarse, Andrew Gerrand, The Go Blog, 4 de agosto de 2010
  8. ^ Python: ¿No hay csv.close()?
  9. ^ "Interfaz desechable" . Consultado el 3 de abril de 2016 .
  10. ^ Código de flecha aplanadora, Jeff Atwood, 10 de enero de 2006

Otras lecturas

enlaces externos