Come ben sappiamo, il framework .NET fornisce un ambiente dove gli oggetti vengono rilasciati quando non sono più utilizzati. Questa gestione automatica della memoria risolve i due errori più comuni delle applicazioni, ossia le perdite di memoria e i riferimenti a memoria non validi.
Questo comunque non ci esime dal prestare particolare attenzione quando abbiamo a che fare con risorse.
Gli ArcObjects sono oggetti COM (codice non gestito) che vengono 'consumati' in ambiente .NET (codice gestito). In sintesi il runtime .NET fornisce delle classi wrapper che fanno da 'ponte' tra il codice gestito (.NET) ed il codice non gestito (COM). Quando si fa riferimento a un oggetto COM, il runtime crea un Runtime Callable Wrapper (RCW) che gestisce il marshalling tra i due ambienti. In modo simile il runtime .NET crea un COM-callable wrappers (CCWs) per la comunicazione da COM a .NET.
Per generare l'RCW viene utilizzato tlbimp.exe (Type Library Importer utility) contenuto nell'SDK di .NET.
Quando facciamo riferimento ad un componente COM in Visual Studio il tlbimp.exe viene eseguito in background. Per identificare un RCW possiamo vedere in debug gli oggetti visualizzati come System.__ComObject.
Con la Type Library Importer utility le COM coClass sono convertite in classi gestite. Il nome delle classi gestite è uguale a quello di quelle non gestite con il suffisso Class. Per esempio l'RCW della classe Envelope è EnvelopeClass.
Nell'ambiente COM, la gestione della durata avviene tramite il conteggio dei riferimenti. Ogni oggetto COM dispone di un conteggio interno dei riferimenti client che lo indicano. Questi conteggi sono aumentati e diminuiti dallo sviluppatore client richiamando IUnknown::AddRef e IUnknown::Release. AddRef incrementa il conteggio e deve essere richiamato quando viene impostato un riferimento al componente, mentre Release diminuisce il conteggio e deve essere richiamato quando il riferimento viene eliminato. Se il conteggio scende a 0, l'oggetto può essere eliminato. (vedi Reference counting )
In ambiente .NET il livello utilizzato è quello di classe e l'impatto sulle prestazioni sarebbe troppo grande se il conteggio dei riferimenti fosse stato aggiunto al CLR. (vedi Garbage Collection )
In ambiente .NET l'oggetto dura finché il Garbage Collector vede che non è più raggiungibile da un riferimento principale forte tramite un percorso di riferimenti forti.
Ritorniamo alla nostra class RCW: AddRef viene chiamato dopo la creazione del componente, mentre Release viene richiamato quando l'RCW è finalizzato.
Il problema è che non sappiamo quando la finalizzazione avviene in ambiente .NET.
Il problema principale è che, se il nostro oggetto trattiene risorse, potrebbe bloccarle e quindi non renderle disponibili.
Gli ambienti .NET ed ESRI mettono a disposizione classi e metodi per una finalizzazione deterministica.
Occorre però prestare molta attenzione a quando utilizzare o meno queste classi/metodi.
Innanzitutto in ambiente .NET abbiamo a disposizione l'interfaccia IDisposable che ci permette, implementandola nella nostra classe, di rilasciare risorse gestite e non. Occorrerà seguire un determinato pattern di sviluppo poichè, se non si dovesse chiamare il metodo Dispose esplicitamente, il Garbage Collector da solo finalizzerà l'oggetto non conoscendo l'interfaccia IDisposable.
Implementando l'interfaccia IDisposable, sarà possibile utilizzare lo using che ne garantirà un utilizzo corretto.
ESRI mette a disposizione la classe ComReleaser che implementa IDisposable
Come visto nel post sul ComReleaser possiamo utilizzare questa classe per finalizzare le risorse in un determinato blocco. In sostanza ESRI ha creato un helper nel quale indichiamo quali sono gli oggetti che desideriamo rilasciare in modo deterministico.
In .NET abbiamo a disposizione il ReleaseComObject e il FinalReleaseComObject.
Il ReleaseComObject decrementa il conteggio dell' RCW associato all'oggetto COM e restituisce il valore del conteggio mentre il FinalReleaseComObject rilascia tutti i riferimenti portando il conteggio a 0, se il rilascio ha avuto successo. Occorre tener presente che l'RCW potrebbe ancora esistere perchè attende il garbage collector.
Occorre prestare attenzione a non utilizzare un COM dopo che è stato separato dal suo RCW, altrimenti si incorre in un eccezione. In questo esempio rilasciamo la FeatureClass table1 e poi successivamente la richiamiamo.
m_AOLicenseInitializer.InitializeApplication(new esriLicenseProductCode[] { esriLicenseProductCode.esriLicenseProductCodeEngine },new esriLicenseExtensionCode[] { });
try
{
IPropertySet propertySet = new PropertySetClass();
propertySet.SetProperty("DATABASE", @"D:\Temp\test.mdb");
Type factoryType = Type.GetTypeFromProgID("esriDataSourcesGDB.AccessWorkspaceFactory");
IWorkspaceFactory workspaceFactory = (IWorkspaceFactory) Activator.CreateInstance(factoryType);
IWorkspace workspace = workspaceFactory.Open(propertySet, 0);
IFeatureWorkspace featureWorkspace = workspace as IFeatureWorkspace;
IFeatureClass table1 = featureWorkspace.OpenFeatureClass("streets");
int k = Marshal.ReleaseComObject(table1);
Console.Write(table1.HasOID);
Console.Read();
}
catch (Exception ex)
{
Console.Write(ex.Message);
Console.Read();
}
m_AOLicenseInitializer.ShutdownApplication();
In .NET è presente il metodo Collect che forza il garbage collector. Questo metodo è da utilizzare esclusivamente solo quando necessario (sono veramente pochi i casi). Nel caso foste interessati, vi consiglio di dare un occhio a questo post per verificare quando utilizzarlo. Nella stessa classe (GC) possiamo notare anche i metodi SuppressFinalize (da utilizzare quando ad esempio implementiamo l'IDisposable e, poichè gestiamo le risorse, desideramo evitare che il garbage collector richiami la finalizzazione sull'oggetto). WaitForPendingFinalizers è un altro metodo che permette di rimanere in attesa sul thread sul quale lo si chiama fino a quando il thread che esegue la finalizzazione degli oggetti disponibili alla finalizzazione ha terminato.
In questo esempio vediamo come i datasets sono pooled nel loro workspace
IPropertySet propertySet = new PropertySetClass();
propertySet.SetProperty("DATABASE", @"D:\Temp\test.mdb");
Type factoryType = Type.GetTypeFromProgID("esriDataSourcesGDB.AccessWorkspaceFactory");
IWorkspaceFactory workspaceFactory = (IWorkspaceFactory) Activator.CreateInstance(factoryType);
IWorkspace workspace = workspaceFactory.Open(propertySet, 0);
IFeatureWorkspace featureWorkspace = workspace as IFeatureWorkspace;
using (ComReleaser comReleaser = new ComReleaser())
{
ITable table1 = featureWorkspace.OpenTable("zTest");
ITable table2 = featureWorkspace.OpenTable("zTest");
if (object.ReferenceEquals(table1, table2))
Console.WriteLine("Stesso oggetto");
Console.WriteLine(Marshal.FinalReleaseComObject(table1));
Console.WriteLine(Marshal.ReleaseComObject(table2));
}
table1 e table2 puntano al riferimento della stessa istanza e, se eseguiamo un FinalReleaseComObject, verranno azzerati tutti riferimenti (vedi il ReleaseComObject successivo che restituisce -1) pertanto il table2 non è utilizzabile
altrimenti si incorre nell'eccezione vista precedentemente.
Come abbiamo detto nel post sul ComReleaser è importante rilasciare i cursori per evitare di lasciare in blocco la tabella alla quale il cursore fa riferimento.
using (ComReleaser comReleaser = new ComReleaser())
{
ICursor cursor = table1.Search(null, true);
comReleaser.ManageLifetime(cursor);
IRow row = null;
bool isManaged = false;
while ((row = cursor.NextRow()) != null)
{
if (!isManaged)
{
comReleaser.ManageLifetime(row);
isManaged = true;
}
// utilizza la row...
}
}
In questo esempio abbiamo un cursore che viene riciclato (parametro true nel Search) e quindi non viene creata una nuova istanza di row ad ogni riga successiva. Pertanto occorre aggiungere la row una sola volta.
Diversamente, se dovessimo utilizzare un cursore non riciclato (parametro false nel Search) occorrerà ad ogni riga successiva gestire il rilascio poichè la row è una nuova istanza.
using (ComReleaser comReleaser = new ComReleaser())
{
ICursor cursor = table1.Search(null, false);
comReleaser.ManageLifetime(cursor);
IRow row = null;
while ((row = cursor.NextRow()) != null)
{
try
{
// utilizza la row...
}
catch (Exception ex)
{
// gestisci l'eccezione...
}
finally
{
Marshal.ReleaseComObject(row);
}
}
}
Pertanto occorre sempre capire quando siamo in presenza di una stessa istanza.
Ad esempio, se ci dovessimo connettere ad ArcSDE in due modi diversi: prima con la connection properties e poi con la connection file con stessa user/password e versione, avremo un singolo COM che sarà condiviso in un singolo RCW. Nel dubbio verificate con un object.ReferenceEquals(obj1,obj2).
Nessun commento:
Posta un commento