¿Es la gestión de recursos no determinista una fuga de abstracción?

9

Por lo que puedo ver, hay dos formas dominantes de gestión de recursos: destrucción determinista y explícita. Ejemplos de lo primero serían destructores de C ++ y punteros inteligentes o el submarino DESTROY de Perl, mientras que un ejemplo de este último sería el paradigma de bloques para administrar recursos de Ruby o la interfaz IDispose de .NET.

Los idiomas más nuevos parecen optar por el último, tal vez como un efecto secundario del uso de sistemas de recolección de basura de la variedad sin recuento de referencias.

Mi pregunta es la siguiente: dado que los destructores para punteros inteligentes o sistemas de recolección de basura con conteo de referencias, que casi son lo mismo, permiten la destrucción de recursos implícita y transparente, ¿es una abstracción menos permeable que los tipos no deterministas que se basan en explícitos ¿notación?

Daré un ejemplo concreto. Si tiene tres subclases de C ++ de una sola superclase, una puede tener una implementación que no necesita ninguna destrucción específica. Quizás hace su magia de otra manera. El hecho de que no necesite ninguna destrucción especial es irrelevante: todas las subclases todavía se usan de la misma manera.

Otro ejemplo usa bloques Ruby. Dos subclases necesitan liberar recursos, por lo que la superclase opta por una interfaz que utiliza un bloque en el constructor, aunque otras subclases específicas no lo necesiten, ya que no requieren una destrucción especial.

¿Es el caso que este último filtra detalles de implementación de la destrucción de recursos, mientras que el primero no?

EDITAR: Comparar, digamos, Ruby con Perl podría ser más justo ya que uno tiene destrucción determinista y el otro no, pero ambos son recolectados de basura.

Louis Jackman
fuente
55
Estoy tentado a decir "sí", pero me gusta escuchar lo que otros tienen que decir sobre esto.
Bart van Ingen Schenau
¿Destrucción transparente de recursos? ¿Aparte del hecho de que tiene que usar punteros inteligentes en lugar de punteros normales? No creo que esto sea más transparente que tener un solo mecanismo (referencias) para acceder a los objetos (en C ++ tiene al menos cuatro o cinco).
Giorgio
@Giorgio: "Formas de acceder a un objeto" es bastante vago. ¿Te refieres a leer o escribir? Const / calificación volátil? Los punteros no son realmente "una forma de acceder a un objeto"; prácticamente cualquier expresión da como resultado un objeto y desreferenciar un puntero simplemente no es tan especial.
MSalters
1
@Giorgio: en ese sentido OOP, no puede enviar un mensaje a un puntero C ++. Debe desreferenciar el puntero para enviar el mensaje (*ptr).Message()o de manera equivalente ptr->Message(). Hay un conjunto infinito de expresiones permitidas, como ((*ptr))->Messagetambién es equivalente. Pero todos se reducen aexpressionIdentifyingAnObject.Message()
MSalters
1
Con el recuento, debe tener cuidado al evitar los círculos. Así que esa abstracción también se filtra, solo que de una manera diferente.
CodesInChaos

Respuestas:

2

Su propio ejemplo responde a la pregunta. La destrucción transparente es claramente menos permeable que la destrucción explícita. Puede tener fugas, pero tiene menos fugas.

La destrucción explícita es análoga a malloc / free en C con todas las trampas. Tal vez con algo de azúcar sintáctico para que parezca basado en el alcance.

Algunos de los beneficios de la destrucción transparente sobre explícita: -
mismo patrón de uso -
no puede olvidarse de liberar el recurso.
Los detalles limpios no ensucian el paisaje en el punto de uso.

mike30
fuente
2

La falla en la abstracción no es en realidad el hecho de que la recolección de basura no es determinista, sino más bien la idea de que los objetos están "interesados" en cosas a las que tienen referencias, y no están interesados ​​en cosas a las que no tienen referencias Para ver por qué, considere el escenario de un objeto que mantiene un contador de la frecuencia con la que se pinta un control en particular. En la creación, se suscribe al evento de "pintura" del control, y al eliminarlo se da de baja. El evento click simplemente incrementa un campo, y un método getTotalClicks()devuelve el valor de ese campo.

Cuando se crea el objeto contador, debe hacer que una referencia a sí mismo se almacene dentro del control que está monitoreando. El control realmente no se preocupa por el objeto contador, y sería igual de feliz si el objeto contador y la referencia a él dejaran de existir, pero mientras exista la referencia, llamará al controlador de eventos de ese objeto cada vez. se pinta solo. Esta acción es totalmente inútil para el control, pero sería útil para cualquiera que llame getTotalClicks()al objeto.

Si, por ejemplo, un método fuera para crear un nuevo objeto "contador de pintura", realizar alguna acción en el control, observar cuántas veces se repintó el control y luego abandonar el objeto contador de pintura, el objeto permanecería suscrito al evento incluso aunque a nadie le importaría si el objeto y todas las referencias a él simplemente desaparecieran. Sin embargo, los objetos no serían elegibles para la colección hasta que el control en sí lo sea. Si el método fuera invocado miles de veces dentro de la vida útil del control [un escenario plausible], podría causar un desbordamiento de memoria, pero por el hecho de que el costo de las invocaciones de N probablemente sería O (N ^ 2) u O (N ^ 3) a menos que el procesamiento de la suscripción fuera muy eficiente y la mayoría de las operaciones en realidad no involucraran ninguna pintura.

Este escenario particular podría manejarse si el control mantiene una referencia débil al objeto del contador en lugar de una fuerte. Un modelo de suscripción débil es útil, pero no funciona en el caso general. Supongamos que, en lugar de querer tener un objeto que supervise un solo tipo de evento desde un solo control, uno quisiera tener un objeto registrador de eventos que supervisara varios controles, y el mecanismo de manejo de eventos del sistema era tal que cada control necesitaba una referencia a un objeto de registrador de eventos diferente. En ese caso, el objeto que vincula un control al registrador de eventos debe permanecer vivo siempre que ambosel control que se monitorea y el registrador de eventos siguen siendo útiles. Si ni el control ni el registrador de eventos tienen una fuerte referencia al evento de vinculación, dejará de existir aunque todavía sea "útil". Si alguno de ellos tiene un evento fuerte, la vida útil del objeto de enlace puede extenderse inútilmente incluso si el otro muere.

Si no existe ninguna referencia a un objeto en ningún lugar del universo, el objeto puede considerarse inútil y eliminarse de la existencia. Sin embargo, el hecho de que exista una referencia a un objeto no implica que el objeto sea "útil". En muchos casos, la utilidad real de los objetos dependerá de la existencia de referencias a otros objetos que, desde la perspectiva de GC, no tienen ninguna relación con ellos.

Si los objetos son notificados determinísticamente cuando nadie está interesado en ellos, podrán usar esa información para asegurarse de que cualquier persona que se beneficie de ese conocimiento esté informado. Sin embargo, en ausencia de dicha notificación, no hay una forma general de determinar qué objetos se consideran "útiles" si se conoce solo el conjunto de referencias que existen, y no el significado semántico asociado a esas referencias. Por lo tanto, cualquier modelo que suponga que la existencia o no existencia de referencias es suficiente para la gestión automatizada de recursos estaría condenado incluso si el GC pudiera detectar instantáneamente el abandono de objetos.

Super gato
fuente
0

Ningún "destructor u otra interfaz que diga" esta clase debe ser destruida "es un contrato de esa interfaz. Si crea un subtipo que no requiere destrucción especial, me inclinaría a considerar que es una violación del Principio de sustitución de Liskov .

En cuanto a C ++ frente a otros, no hay mucha diferencia. C ++ fuerza esa interfaz en todos sus objetos. Las abstracciones no pueden filtrarse cuando el idioma las requiere.

Telastyn
fuente
44
"Si crea un subtipo que no requiere destrucción especial" Eso no es una violación de LSP, ya que no-op es un caso especial de destrucción válido. El problema es cuando agrega el requisito de destrucción a una clase derivada.
CodesInChaos
Me estoy confundiendo aquí. Si uno necesita agregar un código de destrucción especial a una subclase de C ++, no cambia sus patrones de uso en absoluto, porque es automático. Eso significa que la superclase y la subclase todavía se pueden usar indistintamente. Pero con la notación explícita para la gestión de recursos, una subclase que necesita destrucción explícita haría su uso incompatible con una superclase, ¿no? (Suponiendo que la superclase no necesitara destrucción explícita.)
Louis Jackman
@CodesInChaos - ah sí, supongo que es verdad.
Telastyn
@ljackman: una clase que requiere una destrucción especial impone una carga a quien llama a su constructor para asegurarse de que se lleve a cabo. Esto no crea una violación de LSP ya DerivedFooThatRequiresSpecialDestructionque solo se puede crear mediante un código que llama new DerivedFooThatRequiresSpecialDestruction(). Por otro lado, un método de fábrica que devolviera un DerivedFooThatRequiresSpecialDestructioncódigo que no esperaba algo que requiriera destrucción, sería una violación de LSP.
supercat
0

Mi pregunta es la siguiente: dado que los destructores para punteros inteligentes o sistemas de recolección de basura con conteo de referencias, que casi son lo mismo, permiten la destrucción de recursos implícita y transparente, ¿es una abstracción menos permeable que los tipos no deterministas que se basan en explícitos ¿notación?

Tener que observar los ciclos a mano no es implícito ni transparente. La única excepción es un sistema de conteo de referencias con un lenguaje que prohíbe los ciclos por diseño. Erlang podría ser un ejemplo de tal sistema.

Entonces ambos enfoques tienen fugas. La principal diferencia es que los destructores tienen fugas en todas partes en C ++, pero IDisposees muy raro en .NET.

Jon Harrop
fuente
1
Excepto que los ciclos son extremadamente raros y prácticamente nunca ocurren, excepto en estructuras de datos que son explícitamente cíclicas. La principal diferencia es que los destructores en C ++ se manejan correctamente en todas partes, pero IDispose rara vez maneja el problema en .NET.
DeadMG
"Excepto que los ciclos son extremadamente raros". En idiomas modernos? Yo desafiaría esa hipótesis.
Jon Harrop