Inizializzazione differita

L'inizializzazione differita di un oggetto significa che la creazione dell'oggetto viene posticipata finché l'oggetto non viene usato per la prima volta. In questo argomento i termini inizializzazione differita e creazione di istanze differita sono sinonimi. L'inizializzazione differita viene usata in particolare per migliorare le prestazioni, evitare calcoli superflui e ridurre i requisiti di memoria del programma. Ecco gli scenari più comuni:

  • La creazione di un oggetto è dispendiosa e il programma potrebbe non usarlo. Si supponga, ad esempio, di avere un oggetto Customer in memoria che include una proprietà Orders contenente una matrice di grandi dimensioni di oggetti Order, per la cui inizializzazione è necessaria una connessione di database. Se l'utente non chiede mai di visualizzare gli ordini o di usare i dati in un calcolo, non vi è motivo di usare la memoria di sistema o cicli di calcolo per creare l'oggetto. Usando Lazy<Orders> per dichiarare l'oggetto Orders per l'inizializzazione differita, è possibile evitare di sprecare risorse di sistema quando l'oggetto non viene usato.

  • La creazione di un oggetto è dispendiosa e si vuole posticiparla fino al completamento di altre operazioni dispendiose. Si supponga, ad esempio, che il programma carichi diverse istanze dell'oggetto all'avvio, ma che solo alcune siano necessarie immediatamente. È possibile migliorare le prestazioni di avvio del programma posticipando l'inizializzazione degli oggetti non necessari finché non vengono creati quelli necessari.

Benché sia possibile scrivere codice personalizzato per eseguire l'inizializzazione differita, è consigliabile usare Lazy<T>. Lazy<T> e i tipi correlati supportano anche la thread safety e forniscono criteri uniformi di propagazione delle eccezioni.

La tabella seguente elenca i tipi forniti da .NET Framework versione 4 per consentire l'inizializzazione differita in diversi scenari.

Tipo Descrizione
Lazy<T> Classe wrapper che fornisce la semantica di inizializzazione differita per qualsiasi libreria di classi o tipo definito dall'utente.
ThreadLocal<T> È simile a Lazy<T>, con la differenza che fornisce la semantica di inizializzazione differita in base a dati locali del thread. Ogni thread ha accesso al proprio valore univoco.
LazyInitializer Fornisce metodi static (Shared in Visual Basic) avanzati per l'inizializzazione differita di oggetti senza l'overhead di una classe.

Inizializzazione differita di base

Per definire un tipo a inizializzazione differita, ad esempio MyType, usare Lazy<MyType> (Lazy(Of MyType) in Visual Basic), come mostrato nell'esempio seguente. Se non viene passato alcun delegato nel costruttore Lazy<T>, il tipo con wrapping viene creato usando Activator.CreateInstance in occasione del primo accesso alla proprietà del valore. Se il tipo non ha un costruttore senza parametri, viene generata un'eccezione in fase di esecuzione.

Nell'esempio seguente si supponga che Orders sia una classe che contiene 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>();
' Initialize by using default Lazy<T> constructor. The 
'Orders array itself is not created yet.
Dim _orders As Lazy(Of Orders) = New Lazy(Of Orders)()

È anche possibile passare un delegato nel costruttore Lazy<T>, che richiama un overload del costruttore specifico nel tipo con wrapping in fase di creazione, ed eseguire tutti gli altri passaggi di inizializzazione necessari, come mostrato 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));
' Initialize by invoking a specific constructor on Order 
' when Value property is accessed
Dim _orders As Lazy(Of Orders) = New Lazy(Of Orders)(Function() New Orders(100))

Una volta creato l'oggetto Lazy, non viene creata alcuna istanza di Orders fino al momento del primo accesso alla proprietà Value della variabile Lazy. Al primo accesso, il tipo con wrapping viene creato e restituito e quindi archiviato per tutti gli accessi futuri.

// 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.
}
' We need to create the array only if _displayOrders is true
If _displayOrders = True Then
    DisplayOrders(_orders.Value.OrderData)
Else
    ' Don't waste resources getting order data.
End If

Un oggetto Lazy<T> restituisce sempre lo stesso oggetto o valore con cui è stato inizializzato. Di conseguenza, la proprietà Value è di sola lettura. Se in Value è archiviato un tipo riferimento, non è possibile assegnarvi un nuovo oggetto. Tuttavia, è possibile modificare il valore delle proprietà e dei campi pubblici impostabili correlati. Se in Value è archiviato un tipo valore, non è possibile modificarne il valore. È tuttavia possibile creare una nuova variabile richiamando di nuovo il costruttore della variabile con nuovi argomenti.

_orders = new Lazy<Orders>(() => new Orders(10));
_orders = New Lazy(Of Orders)(Function() New Orders(10))

La nuova istanza Lazy, come la precedente, non crea un'istanza di Orders fino al momento del primo accesso alla proprietà Value correlata.

Inizializzazione thread-safe

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

Nota

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

L'esempio seguente mostra che la stessa istanza Lazy<int> ha lo stesso valore per tre thread separati.

// 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.
*/
' Initialize the integer to the managed thread id of the 
' first thread that accesses the Value property.
Dim number As Lazy(Of Integer) = New Lazy(Of Integer)(Function()
                                                          Return Thread.CurrentThread.ManagedThreadId
                                                      End Function)

Dim t1 As New Thread(Sub()
                         Console.WriteLine("number on t1 = {0} threadID = {1}",
                                           number.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
t1.Start()

Dim t2 As New Thread(Sub()
                         Console.WriteLine("number on t2 = {0} threadID = {1}",
                                           number.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
t2.Start()

Dim t3 As New Thread(Sub()
                         Console.WriteLine("number on t3 = {0} threadID = {1}",
                                           number.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
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, usare il tipo ThreadLocal<T>, descritto più avanti in questo argomento.

Alcuni costruttori Lazy<T> includono un parametro booleano chiamato isThreadSafe, che viene usato per specificare se si intende accedere alla proprietà Value da più thread. Se si intende accedere alla proprietà da un solo thread, passare false per ottenere un vantaggio moderato in termini di prestazioni. Se si intende accedere alla proprietà da più thread, passare true per indicare all'istanza Lazy<T> di gestire correttamente le race condition in cui un thread genera un'eccezione in fase di inizializzazione.

Alcuni costruttori Lazy<T> includono un parametro LazyThreadSafetyMode chiamato mode. Questi costruttori forniscono una modalità di thread safety aggiuntiva. La tabella seguente mostra in che modo i parametri del costruttore che specificano la thread safety influiscono sulla thread safety di un oggetto Lazy<T>. Ogni costruttore include al massimo uno di questi parametri.

Thread safety dell'oggetto parametro LazyThreadSafetyModemode Parametro isThreadSafe booleano Nessun parametro di thread safety
Completamente thread-safe. Un solo thread per volta tenta di inizializzare il valore. ExecutionAndPublication true Sì.
Non thread-safe. None false Non applicabile.
Completamente thread-safe. I thread competono per inizializzare il valore. PublicationOnly Non applicabile. Non applicabile.

Come mostrato nella tabella, l'immissione di LazyThreadSafetyMode.ExecutionAndPublication per il parametro mode equivale all'immissione di true per il parametro isThreadSafe, mentre l'immissione di LazyThreadSafetyMode.None equivale all'immissione di false.

Per altre informazioni su ciò a cui fanno riferimento Execution e Publication, vedere LazyThreadSafetyMode.

Se si specifica LazyThreadSafetyMode.PublicationOnly, si consente a più thread di tentare di inizializzare l'istanza Lazy<T>. Solo un thread può riuscire, mentre tutti gli altri thread ricevono il valore inizializzato da quello che ha avuto esito positivo. Se durante l'inizializzazione viene generata un'eccezione in un thread, questo thread non riceve il valore impostato dal thread che è riuscito a eseguire l'inizializzazione. Poiché le eccezioni non sono memorizzate nella cache, un tentativo successivo di accesso alla proprietà Value può restituire un'inizializzazione riuscita. Questo comportamento è diverso dal modo in cui vengono gestite le eccezioni in altre modalità, descritto nella sezione seguente. Per altre informazioni, vedere l'enumerazione LazyThreadSafetyMode.

Eccezioni negli oggetti Lazy

Come già indicato, un oggetto Lazy<T> restituisce sempre lo stesso oggetto o valore con cui è stato inizializzato e di conseguenza la proprietà Value è di sola lettura. Se si abilita la memorizzazione nella cache delle eccezioni, questa natura non modificabile si estende al comportamento delle eccezioni. Se per un oggetto a inizializzazione differita è abilitata la memorizzazione nella cache delle eccezioni e l'oggetto genera un'eccezione dal proprio metodo di inizializzazione al momento del primo accesso alla proprietà Value, la stessa eccezione viene generata nei tentativi di accesso successivi alla proprietà Value. In altre parole, il costruttore del tipo con wrapping non viene mai richiamato, anche in scenari di multithreading. Di conseguenza, l'oggetto Lazy<T> non può generare un'eccezione durante un accesso e restituire un valore all'accesso successivo.

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

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 solo thread o fornire la sincronizzazione. Questo vale per tutti gli aspetti dell'oggetto, anche per la memorizzazione nella cache delle eccezioni.

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 Lazy<T>. In questo caso, le eccezioni non vengono memorizzate nella cache e i tentativi di accesso alla proprietà Value possono continuare finché l'inizializzazione non riesce.

La tabella seguente offre un riepilogo del modo in cui i costruttori Lazy<T> controllano la memorizzazione nella cache delle eccezioni.

Costruttore Modalità di thread safety Usa il metodo di inizializzazione Le eccezioni vengono memorizzate nella cache
Lazy(T)() (ExecutionAndPublication) No No
Lazy(T)(Func(T)) (ExecutionAndPublication)
Lazy(T)(Boolean) True (ExecutionAndPublication) o false (None) No No
Lazy(T)(Func(T), Boolean) True (ExecutionAndPublication) o false (None)
Lazy(T)(LazyThreadSafetyMode) Specificata dall'utente No No
Lazy(T)(Func(T), LazyThreadSafetyMode) Specificata dall'utente No se l'utente specifica PublicationOnly; in caso contrario, sì.

Implementazione di una proprietà a inizializzazione differita

Per implementare una proprietà pubblica usando l'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 additional
            // initialization steps here.
            return new Orders(this.CustomerID);
        });
    }

    public Orders MyOrders
    {
        get
        {
            // Orders is created on first access here.
            return _orders.Value;
        }
    }
}
Class Customer
    Private _orders As Lazy(Of Orders)
    Public Shared CustomerID As String
    Public Sub New(ByVal id As String)
        CustomerID = id
        _orders = New Lazy(Of Orders)(Function()
                                          ' You can specify additional 
                                          ' initialization steps here
                                          Return New Orders(CustomerID)
                                      End Function)

    End Sub
    Public ReadOnly Property MyOrders As Orders

        Get
            Return _orders.Value
        End Get

    End Property

End Class

Poiché la proprietà Value è di sola lettura, la proprietà che la espone non ha funzioni di accesso set. Se è necessaria una proprietà di lettura/scrittura con un oggetto Lazy<T> sottostante, la funzione di accesso set deve creare un nuovo oggetto Lazy<T> e assegnarlo all'archivio sottostante. La funzione di accesso set deve creare un'espressione lambda che restituisca il nuovo valore di proprietà passato alla funzione di accesso set e passare l'espressione lambda al costruttore per il nuovo oggetto Lazy<T>. L'accesso successivo della proprietà Value provoca l'inizializzazione del nuovo oggetto Lazy<T> e da questo momento in poi la proprietà Value correlata restituirà il nuovo valore assegnato alla proprietà. Il motivo di questo comportamento complesso è preservare le protezioni multithreading integrate 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 necessario scrivere codice thread-safe personalizzato a questo scopo. A causa delle inizializzazioni aggiuntive necessarie per una proprietà di lettura/scrittura con un oggetto Lazy<T> sottostante, le prestazioni potrebbero non essere accettabili. Inoltre, a seconda dello scenario specifico, potrebbe essere necessario un ulteriore coordinamento per evitare race condition tra setter e getter.

Inizializzazione differita con dati locali del thread

In alcuni scenari di multithreading potrebbe essere necessario fornire a ogni thread dati privati propri. Questi dati vengono chiamati dati locali del thread. In .NET Framework 3.5 e versioni precedenti è possibile applicare l'attributo ThreadStatic a una variabile statica per renderla una variabile di thread locale. Tuttavia, l'uso dell'attributo ThreadStatic può provocare errori difficili da rilevare. Ad esempio, anche istruzioni di inizializzazione di base possono far sì che la variabile venga inizializzata solo nel primo thread che vi accede, come mostrato nell'esempio seguente.

[ThreadStatic]
static int counter = 1;
<ThreadStatic()>
Shared counter As Integer

In tutti gli altri thread la variabile verrà inizializzata usando il valore predefinito (zero). Come alternativa, in .NET Framework 4 è possibile usare il tipo System.Threading.ThreadLocal<T> per creare una variabile di thread locale basata su istanza che deve essere inizializzata in tutti i thread dal delegato Action<T> specificato personalmente. Nell'esempio seguente tutti i thread che accedono a counter visualizzeranno il valore iniziale come 1.

ThreadLocal<int> betterCounter = new ThreadLocal<int>(() => 1);
Dim betterCounter As ThreadLocal(Of Integer) = New ThreadLocal(Of Integer)(Function() 1)

ThreadLocal<T> esegue il wrapping del proprio oggetto quasi allo stesso modo di Lazy<T>, con queste importanti differenze:

  • Ogni thread inizializza la variabile di thread locale usando i propri dati privati, che non sono accessibili da altri thread.

  • La proprietà ThreadLocal<T>.Value è di lettura/scrittura e può essere modificata tutte le volte che è necessario. Questo può influire sulla propagazione delle eccezioni. Ad esempio, un'operazione get può generare un'eccezione, ma la successiva può inizializzare correttamente il valore.

  • Se non si specifica alcun delegato di inizializzazione, ThreadLocal<T> inizializza il tipo con wrapping usando il valore predefinito del tipo. In questo senso, ThreadLocal<T> è coerente con l'attributo ThreadStaticAttribute.

L'esempio seguente mostra che ogni thread che accede all'istanza ThreadLocal<int> ottiene una 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
*/
' Initialize the integer to the managed thread id on a per-thread basis.
Dim threadLocalNumber As New ThreadLocal(Of Integer)(Function() Thread.CurrentThread.ManagedThreadId)
Dim t4 As New Thread(Sub()
                         Console.WriteLine("number on t4 = {0} threadID = {1}",
                                           threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
t4.Start()

Dim t5 As New Thread(Sub()
                         Console.WriteLine("number on t5 = {0} threadID = {1}",
                                           threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
t5.Start()

Dim t6 As New Thread(Sub()
                         Console.WriteLine("number on t6 = {0} threadID = {1}",
                                           threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
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 

Variabili di thread locali in Parallel.For e ForEach

Quando si usa il metodo Parallel.For o Parallel.ForEach per eseguire l'iterazione delle origini dati in parallelo, è possibile usare gli overload che includono supporto integrato per dati locali del thread. In questi metodi la natura locale dei dati del thread viene ottenuta usando delegati locali per accedere ai dati, crearli e pulirli. Per altre informazioni, vedere Procedura: Scrivere un ciclo Parallel.For con variabili di thread locali e Procedura: Scrivere un ciclo Parallel.ForEach con variabili partition-local.

Uso dell'inizializzazione differita per scenari con sovraccarico ridotto

Negli scenari in cui è necessario inizializzare in modo differito un numero elevato di oggetti, si potrebbe stabilire che il wrapping di ogni oggetto in un oggetto Lazy<T> richiede una quantità eccessiva di memoria o di risorse di calcolo. In un altro caso, potrebbe essere necessario soddisfare requisiti rigorosi relativamente all'esposizione dell'inizializzazione differita. In questi casi, è possibile usare i metodi static (Shared in Visual Basic) della classe System.Threading.LazyInitializer per inizializzare in modo differito ogni oggetto senza eseguirne il wrapping in un'istanza di Lazy<T>.

Invece di eseguire il wrapping di un intero oggetto Orders in un oggetto Lazy<T>, nell'esempio seguente si supponga di aver inizializzato in modo differito singoli oggetti Order solo quando sono 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);
        });
    }
}
' Assume that _orders contains null values, and
' we only need to initialize them if displayOrderInfo is true
If displayOrderInfo = True Then


    For i As Integer = 0 To _orders.Length
        ' Lazily initialize the orders without wrapping them in a Lazy(Of T)
        LazyInitializer.EnsureInitialized(_orders(i), Function()
                                                          ' Returns the value that will be placed in the ref parameter.
                                                          Return GetOrderForIndex(i)
                                                      End Function)
    Next
End If

In questo esempio notare che la procedura di inizializzazione viene richiamata a ogni iterazione del ciclo. In scenari di multithreading, il primo thread che richiama la procedura di inizializzazione è quello i cui valori vengono visualizzati da tutti i thread. Anche i thread successivi richiamano la procedura di inizializzazione, ma i loro risultati non vengono usati. Se questo tipo di possibile race condition non è accettabile, usare l'overload di LazyInitializer.EnsureInitialized, che accetta un argomento booleano e un oggetto di sincronizzazione.

Vedi anche