La gestione delle risorse non deterministiche è un'astrazione che perde?


9

Da quello che posso vedere, ci sono due forme pervasive di gestione delle risorse: distruzione deterministica ed esplicita. Esempi del primo sarebbero i distruttori C ++ e gli smart pointer o il sub DESTROY di Perl, mentre un esempio del secondo sarebbe il paradigma di Ruby per le risorse da bloccare o l'interfaccia IDispose di .NET.

Le lingue più recenti sembrano optare per quest'ultima, forse come effetto collaterale dell'utilizzo di sistemi di raccolta dei rifiuti della varietà senza riferimento.

La mia domanda è questa: dato che i distruttori per i puntatori intelligenti o i sistemi di raccolta dei rifiuti conteggio dei riferimenti - quasi la stessa cosa - consentono la distruzione implicita e trasparente delle risorse, è un'astrazione meno permeabile rispetto ai tipi non deterministici che si basano su espliciti notazione?

Faccio un esempio concreto. Se hai tre sottoclassi C ++ di una singola superclasse, una potrebbe avere un'implementazione che non richiede alcuna distruzione specifica. Forse fa la sua magia in un altro modo. Il fatto che non abbia bisogno di alcuna distruzione speciale è irrilevante: tutte le sottoclassi sono ancora utilizzate allo stesso modo.

Un altro esempio usa i blocchi di Ruby. Due sottoclassi devono liberare risorse, quindi la superclasse opta per un'interfaccia che utilizza un blocco nel costruttore, anche se altre sottoclassi specifiche potrebbero non averne bisogno in quanto non richiedono alcuna distruzione speciale.

È il caso in cui quest'ultimo perde i dettagli dell'implementazione della distruzione delle risorse, mentre il primo no?

EDIT: Paragonando, diciamo, Ruby a Perl potrebbe essere più giusto dal momento che uno ha distruzione deterministica e l'altro no, eppure entrambi sono raccolti insieme.


5
Sono tentato di dire "sì", ma mi piace sentire ciò che gli altri hanno da dire su questo.
Bart van Ingen Schenau,

Distruzione trasparente delle risorse? A parte il fatto che devi usare i puntatori intelligenti invece dei normali puntatori? Non penso che sia più trasparente che avere un solo meccanismo (riferimenti) per accedere agli oggetti (in C ++ ne hai almeno quattro o cinque).
Giorgio,

@Giorgio: "Modi per accedere a un oggetto" è abbastanza vago. Intendi leggere o scrivere? Qualifica Costante / Volatile? I puntatori non sono realmente "un modo per accedere a un oggetto"; praticamente qualsiasi espressione si traduce in un oggetto e il dereferenziamento di un puntatore non è poi così speciale.
Salterio del

1
@Giorgio: in questo senso OOP, non puoi inviare un messaggio a un puntatore C ++. È necessario dereference il puntatore per inviare il messaggio (*ptr).Message()o equivalentemente ptr->Message(). Esiste un insieme infinito di espressioni consentite, come ((*ptr))->Messageanche equivalente. Ma tutti si riducono aexpressionIdentifyingAnObject.Message()
MSalters

1
Con il refcounting devi stare attento a evitare le cerchie. Quindi anche quell'astrazione perde, solo in un modo diverso.
Codici A Caos

Risposte:


2

Il tuo esempio risponde alla domanda. La distruzione trasparente è chiaramente meno permeabile della distruzione esplicita. Può perdere, ma ha meno perdite.

La distruzione esplicita è analoga a malloc / free in C con tutte le insidie. Forse con un po 'di zucchero sintattico per farlo apparire basato sull'ambito.

Alcuni dei vantaggi della distruzione trasparente rispetto all'esplicito: -
stesso modello di utilizzo
- non puoi dimenticare di rilasciare la risorsa.
- i dettagli puliti non sporcano il paesaggio nel punto di utilizzo.


2

Il fallimento nell'astrazione non è in realtà il fatto che la garbage collection non è deterministica, ma piuttosto nell'idea che gli oggetti siano "interessati" a cose a cui fanno riferimento e che non siano interessati a cose a cui non tengono Riferimenti. Per capire perché, considera lo scenario di un oggetto che mantiene un contatore della frequenza con cui viene dipinto un particolare controllo. Alla creazione, si iscrive all'evento "paint" del controllo e, a sua disposizione, annulla l'iscrizione. L'evento click incrementa semplicemente un campo e un metodo getTotalClicks()restituisce il valore di quel campo.

Quando viene creato l'oggetto contatore, deve causare la memorizzazione di un riferimento a se stesso all'interno del controllo che sta monitorando. Al controllo non interessa davvero l'oggetto contatore, e sarebbe altrettanto felice se l'oggetto contatore, e il riferimento ad esso, cessassero di esistere, ma finché esiste il riferimento chiamerà il gestore eventi di quell'oggetto ogni volta si dipinge da solo. Questa azione è totalmente inutile per il controllo, ma sarebbe utile a chiunque vorrebbe mai invocare getTotalClicks()l'oggetto.

Se, ad esempio, un metodo dovesse creare un nuovo oggetto "contatore di vernice", eseguire alcune azioni sul controllo, osservare quante volte il controllo è stato ridipinto e quindi abbandonare l'oggetto contatore di vernice, l'oggetto rimarrebbe abbonato anche all'evento anche se a nessuno sarebbe mai importato se l'oggetto e tutti i riferimenti ad esso fossero semplicemente svaniti. Gli oggetti non diventerebbero idonei per la raccolta, tuttavia, fino a quando non lo sarà il controllo stesso. Se il metodo fosse uno che verrebbe invocato molte migliaia di volte durante la vita del controllo [uno scenario plausibile], potrebbe causare un overflow della memoria ma per il fatto che il costo di N invocazioni sarebbe probabilmente O (N ^ 2) o O (N ^ 3) a meno che l'elaborazione dell'abbonamento non sia stata molto efficiente e la maggior parte delle operazioni in realtà non riguardasse alcun disegno.

Questo particolare scenario potrebbe essere gestito dando al controllo di mantenere un riferimento debole all'oggetto contatore piuttosto che uno forte. Un modello con abbonamento debole è utile, ma non funziona nel caso generale. Supponiamo che invece di voler avere un oggetto che monitora un singolo tipo di evento da un singolo controllo, si desiderasse avere un oggetto logger di eventi che monitorasse diversi controlli e che il meccanismo di gestione degli eventi del sistema fosse tale che ogni controllo necessitasse di un riferimento a un diverso oggetto logger eventi. In tal caso, l'oggetto che collega un controllo al logger eventi dovrebbe rimanere attivo solo finché entrambiil controllo monitorato e il logger eventi rimangono utili. Se né il controllo né il logger di eventi contengono un forte riferimento all'evento di collegamento, cesserà di esistere anche se è ancora "utile". Se uno dei due contiene un evento forte, la durata dell'oggetto di collegamento può essere inutilmente estesa anche se l'altro muore.

Se non esiste alcun riferimento a un oggetto in qualsiasi parte dell'universo, l'oggetto può tranquillamente essere considerato inutile ed eliminato dall'esistenza. Il fatto che esista un riferimento a un oggetto non implica che l'oggetto sia "utile". In molti casi, l'utilità effettiva degli oggetti dipenderà dall'esistenza di riferimenti ad altri oggetti che - dal punto di vista della GC - non sono totalmente correlati a loro.

Se gli oggetti vengono notificati in modo deterministico quando nessuno è interessato a loro, saranno in grado di utilizzare tali informazioni per assicurarsi che chiunque sia interessato a tale conoscenza sia informato. In assenza di tale notifica, tuttavia, non esiste un modo generale per determinare quali oggetti sono considerati "utili" se si conoscono solo l'insieme di riferimenti esistenti e non il significato semantico associato a tali riferimenti. Pertanto, qualsiasi modello che presupponga che l'esistenza o la non esistenza di riferimenti sia sufficiente per la gestione automatizzata delle risorse sarebbe condannato anche se il GC potesse rilevare immediatamente l'abbandono degli oggetti.


0

Nessun "distruttore o altra interfaccia che dice" questa classe deve essere distrutta "è un contratto di tale interfaccia. Se si crea un sottotipo che non richiede una distruzione speciale, sarei propenso a considerare che una violazione del principio di sostituzione di Liskov .

Per quanto riguarda C ++ rispetto ad altri, non c'è molta differenza. Il C ++ forza quell'interfaccia su tutti i suoi oggetti. Le astrazioni non possono fuoriuscire quando sono richieste dalla lingua.


4
"Se si crea un sottotipo che non richiede una distruzione speciale" Non si tratta di una violazione di LSP, dato che il no-op è un caso speciale di distruzione valido. Il problema è quando si aggiunge il requisito di distruzione a una classe derivata.
Codici A Caos

Mi sto confondendo qui. Se è necessario aggiungere un codice di distruzione speciale a una sottoclasse C ++, non cambia affatto i suoi schemi di utilizzo, perché è automatico. Ciò significa che la superclasse e la sottoclasse possono ancora essere utilizzate in modo intercambiabile. Ma con la notazione esplicita per la gestione delle risorse, una sottoclasse che necessita di una distruzione esplicita renderebbe il suo utilizzo incompatibile con una superclasse, no? (Supponendo che la superclasse non necessitasse di una distruzione esplicita.)
Louis Jackman,

@CodesInChaos - ah sì, suppongo sia vero.
Telastyn,

@ljackman: una classe che richiede una distruzione speciale impone un peso a chi chiama il suo costruttore per assicurarsi che venga eseguita. Ciò non crea una violazione LSP poiché DerivedFooThatRequiresSpecialDestructionpuò essere creato solo da un codice che chiama new DerivedFooThatRequiresSpecialDestruction(). D'altra parte, un metodo di fabbrica che restituiva un DerivedFooThatRequiresSpecialDestructioncodice che non si aspettava qualcosa che richiedesse la distruzione sarebbe una violazione di LSP.
Supercat,

0

La mia domanda è questa: dato che i distruttori per i puntatori intelligenti o i sistemi di raccolta dei rifiuti conteggio dei riferimenti - quasi la stessa cosa - consentono la distruzione implicita e trasparente delle risorse, è un'astrazione meno permeabile rispetto ai tipi non deterministici che si basano su espliciti notazione?

Dover guardare i cicli a mano non è né implicito né trasparente. L'unica eccezione è un sistema di conteggio dei riferimenti con un linguaggio che proibisce i cicli in base alla progettazione. Erlang potrebbe essere un esempio di tale sistema.

Quindi entrambi gli approcci perdono. La differenza principale è che i distruttori perdono ovunque in C ++ ma IDisposeè molto raro su .NET.


1
Tranne i cicli sono estremamente rari e praticamente non si verificano quasi mai se non nelle strutture di dati esplicitamente cicliche. La differenza principale è che i distruttori in C ++ sono gestiti correttamente ovunque, ma IDispose raramente gestisce il problema in .NET.
DeadMG

"Tranne i cicli sono estremamente rari". Nelle lingue moderne? Sfiderei questa ipotesi.
Jon Harrop,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.