Cleanup deterministico e finalizers in C++/CLI

Vediamo il seguente codice. Viene definito un reference type (typeid = Person), con un metodo publico, ReadMessage. Al suo interno viene utilizzato in tre modi diversi un oggetto di tipo System::IO::StreamReader, classe che implementa System::IDisposable.

public ref class Person
{
public:
    
Person (StringnameStringsurname)
    {
    }
    
void ReadMessage (Stringpath)
    {
        
Streamstream = (Person::typeid)->Assembly->GetManifestResourceStream (path);

#ifdef MANUAL_DETERMINISTIC_CLEANUP
        
// I manually call the distructor.
        
StreamReaderreader gcnew StreamReader (stream);
        
Console::WriteLine (reader->ReadLine ());
        
delete reader;
#endif

#ifdef AUTOMATIC_CLEANUP
        
// Garabace collector will call the dispose methods before finalizing the reader.
        
StreamReaderreader gcnew StreamReader (stream);
        
Console::WriteLine (reader->ReadLine ());
#endif

#ifdef AUTOMATIC_DETERMINISTIC_CLEANUP
        
// The compiler automatic inserts a method call to dispose.
        // The reference type instance has value semantic in this context.
        
StreamReader reader (stream);
        
Console::WriteLine (reader.ReadLine ());
#endif
    
}
};

Decompiliamo con Reflector e vediamo come apparirebbe, nei diversi casi, il metodo ReadMessage se fosse scritto in C#.

Nel primo caso reader è stato dichiarato come un handle, al quale abbiamo è stato assegnato un oggetto allocato sul managed heap tramite l'operatore gcnew . Dopo essere stato utilizzato, l'oggetto viene "distrutto" con l'operatore delete. Tale operatore viene sostanzialmente tradotto in un'eventuale chiamata al metodo Dispose (per gli oggetti il cui tipo implementa IDisposable*). Come noto, tale metodo nasce con lo scopo fondamentale di rilascare le risorse utilizzate dal nostro oggetto, non la memoria. La deallocazione sarà gestita dal garbage collector, che in questo caso, vedendo che è già stato eseguito il cleanup, non chiamerà  il distruttore o l'eventuale finalizzatore **. Codice C#:

public void ReadMessage(string path)
{
      
int num1;
      Stream stream1 = 
null;
      StreamReader reader1 = 
null;
      stream1 = 
typeof(Person).Assembly.GetManifestResourceStream(path);
      reader1 = 
new StreamReader(stream1);
      Console.WriteLine(reader1.ReadLine());
      IDisposable disposable1 = reader1;
      
if (disposable1 != null)
      {
            disposable1.Dispose();
            num1 = 0;
      }
      
else
      
{
            num1 = 0;
      }
}

Nel secondo caso, invece, non distruggendo l'oggetto, lasciamo che sia il garbage collector a scaricare le risorse invocando il distruttore prima della finalizzazione. Ciò signfica che fino a un tempo imprecisato le nostre risorse rimarrano utilizzate . Codice C#:

public void ReadMessage(string path)
{
      Stream stream1 = 
null;
      StreamReader reader1 = 
null;
      stream1 = 
typeof(Person).Assembly.GetManifestResourceStream(path);
      reader1 = 
new StreamReader(stream1);
      Console.WriteLine(reader1.ReadLine());
}

Nel terzo caso invece vediamo un reference type object dichiarato come se fosse di un tipo valore. In realtà, questa è una semplice "illusione" propinataci dal compilatore. Il nostro oggetto è allocato sul managed heap, ma la semantica che esso assume, dal nostro punto di vista, è quella di un oggetto sullo stack. Questo ha un'importante implicazione: il compilatore genera automaticamente il codice per rilasciare le risorse usate dal nostro oggetto (ovviamente solo se necessario, ovvero se implementa IDisposable ), esattamente come il C++ nativo inserisce una chiamata ai distruttori delle variabili automatiche alla fine del metodo. Questo idioma, noto come deterministic finalization, è diventato in C++/CLI un nuovo idioma, analogo ma differente (a causa della diversa implementazione dell'operatore delete), detto deterministic cleanup . Codice C#:

public void ReadMessage(string path)
{
      
Stream stream1 null;
      
StreamReader reader1 null;
      
stream1 typeof(Person).Assembly.GetManifestResourceStream(path);
      
StreamReader modopt(IsConstlocal1 = (StreamReader modopt(IsConst)) new StreamReader(stream1);
      
try
      
{
            
reader1 local1;
            
Console.WriteLine(reader1.ReadLine());
      }
      
fault
      
{
            
reader1.Dispose();
      }
      
reader1.Dispose();
}

* In C++/CLI una classe dichiarata con un distruttore, è mappata automaticamente in una classe che implementa IDisposable e il suo distruttore è mappato nel metodo Dispose.

** In C++/CLI è possibile dichiarare un finalizzatore . In questo modo, quando l'oggetto sta per essere distrutto non in maniera deterministica, bensì appena prima di una garbage collection è possibile svolgere un po' di custom behavior (per esempio tracciarlo)... oltre a invocare, com'è naturale, il distruttore affinchè vengano rilasciate le risorse.

Questo è possibile perchè il compilatore genera in questo modo il metodo Dispose:

protected virtual void Dispose([MarshalAs(UnmanagedType.U1)] bool flag1)
{
      
if (flag1)
      {
            
this.~Person();
      }
      
else
      
{
            
try
            
{
                  
this.!Person();
            }
            
finally
            
{
                  
base.Finalize();
            }
      }
}

Anzichè quello che avremmo in assenza di un finalizzatore :

protected virtual void Dispose([MarshalAs(UnmanagedType.U1)] bool flag1)
{
      
if (flag1)
      {
            
this.~Person();
      }
      
else
      
{
            
base.Finalize();
      }
}

Vediamo quindi il seguente esempio:

using namespace System;

public ref class Person
{
private:
    
Stringname_;

public:
    
Person (Stringname)name_ (name)
    {
    }

    ~
Person ()
    {
        
Console::WriteLine (name_ ": I'm releasing resources...!");
    }

    !
Person ()
    {
        
Console::WriteLine (name_ ": I'm going to be finalized...!");
        
Console::WriteLine (name_ ": I will call the destructor...!");
        
delete this;
    }
};

void DeterministicCleanupBehavior ()
{
    
Person person ("Guy on the heap with stack semantic");
}

void NonDeterministicCleanupBehavior ()
{
    
Personperson gcnew Person ("Guy on the heap");
}

int main(array<System::String ^> ^args)
{
    
Console::WriteLine ("Began!");
    
DeterministicCleanupBehavior ();
    
NonDeterministicCleanupBehavior ();
    
Console::WriteLine ("Ended!");
    
return 0;
}

Produrrà il seguente output:

Began!
Guy on the heap with stack semantic: I'm releasing resources...!
Ended!
Guy on the heap: I'm going to be finalized...!
Guy on the heap: I will call the destructor...!
Guy on the heap: I'm releasing resources...!

In realtà non potremmo prevedere quando viene finalizzato dal garbage collector il nostro "guy on the heap"... ma essendo il programma molto breve è naturale che venga scaricato soltanto alla fine dell'esecuzione (è ovvio che se dopo la chiamata a NonDeterministicCleanupBehavior il nostro programma eseguisse una lunga serie di istruzioni sarebbe probabile che il "guy on the heap" venga finalizzato in un qualche istante prima della terminazione del processo).

Print | posted on venerdì 6 maggio 2005 04:05