Lazy Initialization

 

Inizializzazione differita di un oggetto significa che la creazione dell'oggetto viene posticipata al primo utilizzo dello stesso. In questo argomento, i termini inizializzazione differita e creazione di istanze differita sono sinonimi. L'inizializzazione differita viene utilizzata principalmente per migliorare le prestazioni, evitare calcoli inutili e ridurre i requisiti di memoria del programma. Di seguito sono riportati gli scenari più comuni:

  • Quando si ha un oggetto dispendioso da creare, che il programma potrebbe non utilizzare. Si supponga ad esempio di avere in memoria un oggetto Customer con una proprietà Orders contenente una grande matrice di oggetti Order che, per essere inizializzato, richiede una connessione di database. Se l'utente non chiede mai di visualizzare gli ordini o di utilizzare i dati in un calcolo, non vi è alcun motivo di utilizzare la memoria di sistema o i cicli di calcolo per crearlo. Utilizzando Lazy<Orders> per dichiarare l'oggetto Orders per l'inizializzazione differita, si può evitare di sprecare risorse di sistema quando l'oggetto non viene utilizzato.

  • Quando si ha un oggetto dispendioso da creare e si desidera posticiparne la creazione fino a che altre operazioni dispendiose non sono state completate. Si supponga ad esempio che il programma carichi diverse istanze dell'oggetto al suo avvio, ma che solo alcune di esse siano necessarie nell'immediato. È possibile migliorare le prestazioni di avvio del programma posticipando l'inizializzazione degli oggetti non necessari fino a che gli oggetti necessari non sono stati creati.

Sebbene sia possibile scrivere codice personalizzato per eseguire l'inizializzazione differita, si consiglia comunque di utilizzare Lazy<T>. Lazy<T> e i tipi correlati supportano inoltre la thread safety e forniscono criteri di propagazione delle eccezioni coerenti.

Nella tabella seguente vengono elencati i tipi forniti da .NET Framework versione 4 per abilitare l'inizializzazione differita in diversi scenari.

TypeDescrizione
Lazy<T>Classe wrapper che fornisce la semantica dell'inizializzazione differita per tutte le librerie di classi o i tipi definiti dall'utente.
ThreadLocal<T>Simile a Lazy<T>, eccetto per il fatto che fornisce la semantica dell'inizializzazione differita su base locale dei thread. Ogni thread può accedere al proprio valore univoco.
LazyInitializerFornisce metodi static (Shared in Visual Basic) avanzati per l'inizializzazione differita di oggetti senza il sovraccarico di una classe.

Per definire un tipo inizializzato in modalità differita, ad esempio MyType, utilizzare Lazy<MyType> (Lazy(Of MyType) in Visual Basic), come illustrato nell'esempio seguente. Se nessun delegato viene passato nel costruttore Lazy<T>, il tipo di cui è stato eseguito il wrapping viene creato tramite Activator.CreateInstance nel momento in cui si accede per la prima volta alla proprietà di valore. Se il tipo non dispone di un costruttore predefinito, viene generata un'eccezione in fase di esecuzione.

Nell'esempio seguente, si supponga che Orders sia una classe contenente una matrice di oggetti Order recuperati da un database. Un oggetto Customer contiene un'istanza di Orders, ma a seconda delle azioni dell'utente, i dati dell'oggetto Orders potrebbero non essere necessari.

            // Initialize by using default Lazy<T> constructor. The 
            // Orders array itself is not created yet.
            Lazy<Orders> _orders = new Lazy<Orders>();

È inoltre possibile passare un delegato nel costruttore Lazy<T> che richiama un overload del costruttore specifico sul tipo di cui è stato eseguito il wrapping in fase di creazione, quindi eseguire qualunque altro passaggio dell'inizializzazione richiesto, come illustrato nell'esempio seguente.

            // Initialize by invoking a specific constructor on Order when Value
            // property is accessed
            Lazy<Orders> _orders = new Lazy<Orders>(() => new Orders(100));

Una volta creato l'oggetto Lazy, non verrà creata alcuna istanza di Orders fino a che l'accesso alla proprietà Value della variabile Lazy non sarà stato eseguito per la prima volta. Al primo accesso, il tipo di cui è stato eseguito il wrapping verrà creato, restituito e archiviato per un accesso futuro.

            // We need to create the array only if displayOrders is true
            if (displayOrders == true)
            {
                DisplayOrders(_orders.Value.OrderData);
            }
            else
            {
                // Don't waste resources getting order data.
            }

Un oggetto Lazy<T> restituisce sempre lo stesso oggetto o valore con il quale è stato inizializzato. Pertanto, la proprietà Value è di sola lettura. Se Value archivia un tipo di riferimento, non è possibile assegnargli un nuovo oggetto. Tuttavia, è possibile modificare il valore delle proprietà e dei campi pubblici impostabili. Se Value archivia un tipo di valore, non è possibile modificarne il valore. Ciononostante, è possibile creare una nuova variabile richiamando nuovamente il costruttore di variabili tramite nuovi argomenti.

            _orders = new Lazy<Orders>(() => new Orders(10));

La nuova istanza differita, al pari di quella precedente, non crea alcuna istanza di Orders fino a che non viene eseguito il primo accesso alla proprietà Value.

Inizializzazione thread-safe

Per impostazione predefinita, gli oggetti Lazy<T> sono thread-safe. Ciò significa che se il costruttore non specifica il tipo di thread safety, gli oggetti Lazy<T> creati sono thread-safe. Negli scenari multithreading, il primo thread che accede alla proprietà Value di un oggetto Lazy<T> thread-safe la inizializza per tutti gli accessi successivi in tutti i thread e tutti i thread condividono gli stessi dati. Non importa quindi quale thread inizializza l'oggetto, e le race condition sono benigne.

System_CAPS_ICON_note.jpg Nota

È possibile estendere questa coerenza alle condizioni di errore utilizzando la memorizzazione delle eccezioni nella cache. Per ulteriori informazioni, vedere la sezione successiva, Eccezioni negli oggetti Lazy.

Nell'esempio seguente viene illustrato come la stessa istanza di Lazy<int> abbia lo stesso valore per tre thread distinti.

            // Initialize the integer to the managed thread id of the 
            // first thread that accesses the Value property.
            Lazy<int> number = new Lazy<int>(() => Thread.CurrentThread.ManagedThreadId);

            Thread t1 = new Thread(() => Console.WriteLine("number on t1 = {0} ThreadID = {1}",
                                                    number.Value, Thread.CurrentThread.ManagedThreadId));
            t1.Start();

            Thread t2 = new Thread(() => Console.WriteLine("number on t2 = {0} ThreadID = {1}",
                                                    number.Value, Thread.CurrentThread.ManagedThreadId));
            t2.Start();

            Thread t3 = new Thread(() => Console.WriteLine("number on t3 = {0} ThreadID = {1}", number.Value,
                                                    Thread.CurrentThread.ManagedThreadId));
            t3.Start();

            // Ensure that thread IDs are not recycled if the 
            // first thread completes before the last one starts.
            t1.Join();
            t2.Join();
            t3.Join();

            /* Sample Output:
                number on t1 = 11 ThreadID = 11
                number on t3 = 11 ThreadID = 13
                number on t2 = 11 ThreadID = 12
                Press any key to exit.
            */

Se sono necessari dati separati in ogni thread, utilizzare il tipo ThreadLocal<T>, come descritto più avanti in questo argomento.

Alcuni costruttori Lazy<T> dispongono di un parametro booleano denominato isThreadSafe, utilizzato per specificare se l'accesso alla proprietà Value sarà eseguito da più thread. Se si intende accedere alla proprietà da un unico thread, passare false per ottenere un modesto vantaggio in termini di prestazioni. Se si intende accedere alla proprietà da più thread, passare true per indicare all'istanza di Lazy<T> di gestire correttamente le race condition in cui un thread genera un'eccezione in fase di inizializzazione.

Alcuni costruttori Lazy<T> dispongono di un parametro LazyThreadSafetyMode denominato mode. Questi costruttori forniscono una modalità di thread safety aggiuntiva. Nella tabella seguente viene illustrato come la modalità thread safety di un oggetto Lazy<T> venga influenzata dai parametri del costruttore che specificano tale modalità. Ciascun costruttore dispone al massimo di uno di questi parametri.

Thread safety dell'oggettoParametro mode di LazyThreadSafetyModeParametro isThreadSafe booleanoNessun parametro di thread safety
Completamente thread-safe; solo un thread alla volta tenta di inizializzare il valore.ExecutionAndPublicationtrueSì.
Non thread-safe.NonefalseNon applicabile.
Completamente thread-safe; i thread concorrono per inizializzare il valore.PublicationOnlyNon applicabile.Non applicabile.

Come illustrato nella tabella, la specifica di LazyThreadSafetyMode.ExecutionAndPublication per il parametro mode corrisponde alla specifica di true per il parametro isThreadSafe e la specifica di LazyThreadSafetyMode.None corrisponde alla specifica di false.

La specifica di LazyThreadSafetyMode.PublicationOnly consente a più thread di tentare l'inizializzazione dell'istanza di Lazy<T>. Solo un thread può raggiungere lo scopo e tutti gli altri ricevono il valore inizializzato da tale thread. Se viene generata un'eccezione in un thread durante l'inizializzazione, quel thread non riceve il valore impostato dal thread di inizializzazione. Le eccezioni non vengono memorizzate nella cache, pertanto un tentativo di accesso successivo alla proprietà Value può comportare l'esito positivo dell'inizializzazione. Questo non corrisponde al modo in cui le eccezioni vengono gestite in altre modalità, come descritto nella sezione seguente. Per ulteriori informazioni, vedere l'enumerazione LazyThreadSafetyMode.

Come detto in precedenza, un oggetto Lazy<T> restituisce sempre lo stesso oggetto o valore con il quale è stato inizializzato, pertanto la proprietà Value è di sola lettura. Se si abilita la memorizzazione delle eccezioni nella cache, questa immutabilità si estende anche al comportamento delle eccezioni. Se per un oggetto inizializzato in modalità differita è abilitata la memorizzazione delle eccezioni nella cache e l'oggetto genera un'eccezione dal relativo metodo di inizializzazione quando viene eseguito per la prima volta l'accesso alla proprietà Value, la stessa eccezione viene generata a ogni tentativo successivo di accedere alla proprietà Value. In altre parole, il costruttore del tipo di cui è stato eseguito il wrapping non viene mai richiamato, neppure negli scenari multithreading. L'oggetto Lazy<T> non può pertanto generare un'eccezione per un accesso e restituire un valore per un accesso successivo.

La memorizzazione delle eccezioni nella cache è abilitata quando si utilizza un costruttore System.Lazy<T> che accetta un metodo di inizializzazione (parametro valueFactory). È ad esempio abilitata quando si utilizza il costruttore Lazy(T)(Func(T)). Se il costruttore accetta inoltre un valore LazyThreadSafetyMode (parametro mode), specificare LazyThreadSafetyMode.None o LazyThreadSafetyMode.ExecutionAndPublication. Specificando un metodo di inizializzazione, viene abilitata la memorizzazione delle eccezioni nella cache per queste due modalità. Il metodo di inizializzazione può essere molto semplice. Può ad esempio chiamare il costruttore predefinito per T: new Lazy<Contents>(() => new Contents(), mode) in C# o New Lazy(Of Contents)(Function() New Contents()) in Visual Basic. Se si utilizza un costruttore System.Lazy<T> che non specifica un metodo di inizializzazione, le eccezioni generate dal costruttore predefinito per T non vengono memorizzate nella cache. Per ulteriori informazioni, vedere l'enumerazione LazyThreadSafetyMode.

System_CAPS_ICON_note.jpg Nota

Se si crea un oggetto Lazy<T> con il parametro del costruttore isThreadSafe impostato su false o il parametro del costruttore mode impostato su LazyThreadSafetyMode.None, è necessario accedere all'oggetto Lazy<T> da un singolo thread o fornire la sincronizzazione. Questa procedura si applica a tutti gli aspetti dell'oggetto, inclusa la memorizzazione delle eccezioni nella cache.

Come indicato nella sezione precedente, gli oggetti Lazy<T> creati specificando LazyThreadSafetyMode.PublicationOnly gestiscono le eccezioni in modo diverso. Con PublicationOnly, più thread possono competere per inizializzare l'istanza di Lazy<T>. In questo caso, le eccezioni non vengono memorizzate nella cache e i tentativi di accesso successivo alla proprietà Value possono continuare finché l'inizializzazione non ha esito positivo.

Nella tabella seguente vengono riepilogati i modi in cui i costruttori Lazy<T> controllano la memorizzazione delle eccezioni nella cache.

CostruttoreModalità di thread safetyUtilizza il metodo di inizializzazioneLe eccezioni vengono memorizzate nella cache
Lazy(T)()([F: System.Threading.LazyThreadSafetyMode.ExecutionAndPublication](assetId:///F: System.Threading.LazyThreadSafetyMode.ExecutionAndPublication?qualifyHint=False&autoUpgrade=True))NoNo
Lazy(T)(Func(T))([F: System.Threading.LazyThreadSafetyMode.ExecutionAndPublication](assetId:///F: System.Threading.LazyThreadSafetyMode.ExecutionAndPublication?qualifyHint=False&autoUpgrade=True))YesYes
Lazy(T)(Boolean)True ([F: System.Threading.LazyThreadSafetyMode.ExecutionAndPublication](assetId:///F: System.Threading.LazyThreadSafetyMode.ExecutionAndPublication?qualifyHint=False&autoUpgrade=True)) o false ([F: System.Threading.LazyThreadSafetyMode.None](assetId:///F: System.Threading.LazyThreadSafetyMode.None?qualifyHint=False&autoUpgrade=True))NoNo
Lazy(T)(Func(T), Boolean)True ([F: System.Threading.LazyThreadSafetyMode.ExecutionAndPublication](assetId:///F: System.Threading.LazyThreadSafetyMode.ExecutionAndPublication?qualifyHint=False&autoUpgrade=True)) o false ([F: System.Threading.LazyThreadSafetyMode.None](assetId:///F: System.Threading.LazyThreadSafetyMode.None?qualifyHint=False&autoUpgrade=True))YesYes
Lazy(T)(LazyThreadSafetyMode)Specificata dall'utenteNoNo
Lazy(T)(Func(T), LazyThreadSafetyMode)Specificata dall'utenteYesNo se l'utente specifica [F: System.Threading.LazyThreadSafetyMode.PublicationOnly](assetId:///F: System.Threading.LazyThreadSafetyMode.PublicationOnly?qualifyHint=False&autoUpgrade=True); in caso contrario, sì.

Per implementare una proprietà pubblica tramite inizializzazione differita, definire il campo sottostante della proprietà come Lazy<T> e restituire la proprietà Value dalla funzione di accesso get della proprietà.

        class Customer
        {
            private Lazy<Orders> _orders;
            public string CustomerID {get; private set;}
            public Customer(string id)
            {
                CustomerID = id;
                _orders = new Lazy<Orders>(() =>
                {
                    // You can specify any additonal 
                    // initialization steps here.
                    return new Orders(this.CustomerID);
                });
            }

            public Orders MyOrders
            {
                get
                {
                    // Orders is created on first access here.
                    return _orders.Value;
                }
            }
        }

La proprietà Value è di sola lettura; di conseguenza, la proprietà che la espone non possiede alcuna funzione di accesso set. Se è necessaria una proprietà di lettura/scrittura supportata da un oggetto Lazy<T>, la funzione di accesso set deve creare un nuovo oggetto Lazy<T> e assegnarlo all'archivio di backup. La funzione di accesso set deve creare un'espressione lambda che restituisce il nuovo valore della proprietà passato alla funzione di accesso set e passare tale espressione al costruttore per il nuovo oggetto Lazy<T>. Il successivo accesso alla proprietà Value causerà l'inizializzazione del nuovo oggetto Lazy<T> e la relativa proprietà Value restituirà il nuovo valore assegnato alla proprietà. Lo scopo di questa soluzione complessa è mantenere le protezioni di multithreading predefinite in Lazy<T>. In caso contrario, le funzioni di accesso della proprietà dovrebbero memorizzare nella cache il primo valore restituito dalla proprietà Value e modificare solo il valore memorizzato nella cache e sarebbe inoltre necessario scrivere codice thread-safe personalizzato. Date le inizializzazioni aggiuntive richieste da una proprietà di lettura/scrittura supportata da un oggetto Lazy<T>, le prestazioni potrebbero non essere accettabili. Inoltre, a seconda dello scenario specifico, potrebbe essere necessario un coordinamento aggiuntivo per evitare race condition tra proprietà SET e GET.

In alcuni scenari multithreading, potrebbe essere necessario dare a ogni thread i propri dati privati. Tali dati sono definiti dati locali dei thread. In .NET Framework versione 3.5 e precedenti, è possibile applicare l'attributo ThreadStatic a una variabile statica per renderla locale a livello di thread. Tuttavia, l'utilizzo dell'attributo ThreadStatic può causare errori non immediatamente evidenti. Ad esempio, anche le istruzioni di inizializzazione di base causeranno l'inizializzazione della variabile solo nel primo thread che vi accede, come illustrato nell'esempio seguente.

        [ThreadStatic]
        static int counter = 1;

In tutti gli altri thread, la variabile verrà inizializzata tramite il valore predefinito (zero). In alternativa, in .NET Framework versione 4, è possibile utilizzare il tipo System.Threading.ThreadLocal<T> per creare una variabile locale dei thread basata su istanze che venga inizializzata in tutti i thread dal delegato Action<T> specificato. Nell'esempio seguente, tutti i thread che accedono a counter vedranno il valore iniziale 1.

        ThreadLocal<int> betterCounter = new ThreadLocal<int>(() => 1);

ThreadLocal<T> esegue il wrapping del proprio oggetto in modo molto simile a Lazy<T>, con queste differenze sostanziali:

  • Ogni thread inizializza la variabile locale dei thread tramite i propri dati privati, che non sono accessibili dagli altri thread.

  • La proprietà ThreadLocal<T>.Value è di lettura/scrittura e può essere modificata un numero illimitato di volte. Ciò può influire sulla propagazione delle eccezioni; ad esempio, l'operazione get può generare un'eccezione mentre l'operazione successiva può correttamente inizializzare il valore.

  • Se non viene fornito alcun delegato di inizializzazione, ThreadLocal<T> inizializzerà il tipo di cui è stato eseguito il wrapping tramite il valore predefinito del tipo. A questo proposito, ThreadLocal<T> è coerente con l'attributo ThreadStaticAttribute.

Nell'esempio seguente viene illustrato come ogni thread che accede all'istanza di ThreadLocal<int> ottenga la propria copia univoca dei dati.

            // Initialize the integer to the managed thread id on a per-thread basis.
            ThreadLocal<int> threadLocalNumber = new ThreadLocal<int>(() => Thread.CurrentThread.ManagedThreadId);
            Thread t4 = new Thread(() => Console.WriteLine("threadLocalNumber on t4 = {0} ThreadID = {1}",
                                                threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId));
            t4.Start();

            Thread t5 = new Thread(() => Console.WriteLine("threadLocalNumber on t5 = {0} ThreadID = {1}",
                                                threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId));
            t5.Start();

            Thread t6 = new Thread(() => Console.WriteLine("threadLocalNumber on t6 = {0} ThreadID = {1}",
                                                threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId));
            t6.Start();

            // Ensure that thread IDs are not recycled if the 
            // first thread completes before the last one starts.
            t4.Join();
            t5.Join();
            t6.Join();

            /* Sample Output:
               threadLocalNumber on t4 = 14 ThreadID = 14 
               threadLocalNumber on t5 = 15 ThreadID = 15
               threadLocalNumber on t6 = 16 ThreadID = 16 
            */

Quando si utilizza il metodo Parallel.For o Parallel.ForEach per scorrere le origini dati in parallelo, è possibile utilizzare gli overload che dispongono di supporto incorporato per i dati locali dei thread. In questi metodi, la località dei thread si ottiene utilizzando delegati locali per creare, accedere e pulire i dati. Per ulteriori informazioni, vedere How to: Write a Parallel.For Loop with Thread-Local Variables e How to: Write a Parallel.ForEach Loop with Thread-Local Variables.

Negli scenari in cui è necessario inizializzare in modalità differita un gran numero di oggetti, si può stabilire che l'esecuzione del wrapping di ogni oggetto in Lazy<T> richieda troppa memoria o troppe risorse di elaborazione. Oppure, è possibile che siano previsti requisiti severi circa l'esposizione dell'inizializzazione differita. In tali casi, è possibile utilizzare i metodi static (Shared in Visual Basic) della classe System.Threading.LazyInitializer per inizializzare in modalità differita ogni oggetto senza eseguirne il wrapping in un'istanza di Lazy<T>.

Nell'esempio seguente, si supponga che, anziché eseguire il wrapping di un intero oggetto Orders in un unico oggetto Lazy<T>, si abbiano singoli oggetti Order inizializzati in modalità differita solo se necessari.

            // Assume that _orders contains null values, and
            // we only need to initialize them if displayOrderInfo is true
            if(displayOrderInfo == true)
            {
                for (int i = 0; i < _orders.Length; i++)
                {
                    // Lazily initialize the orders without wrapping them in a Lazy<T>
                    LazyInitializer.EnsureInitialized(ref _orders[i], () =>
                        {
                            // Returns the value that will be placed in the ref parameter.
                            return GetOrderForIndex(i);
                        });
                }
            }

In questo esempio, la procedura di inizializzazione viene richiamata su ogni iterazione del ciclo. Negli scenari multithreading, il primo thread a richiamare la procedura di inizializzazione è quello il cui valore viene visto da tutti i thread. Anche gli altri thread richiamano la procedura di inizializzazione, ma i loro risultati non vengono utilizzati. Se questo tipo di race condition potenziale non è accettabile, utilizzare l'overload di LazyInitializer.EnsureInitialized<T> che accetta un argomento booleano e un oggetto di sincronizzazione.

Managed Threading Basics
Threads and Threading
Task Parallel Library (TPL)
How to: Perform Lazy Initialization of Objects

Mostra: