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 oggettiOrder
, 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. UsandoLazy<Orders>
per dichiarare l'oggettoOrders
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 LazyThreadSafetyMode mode |
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) | Sì | Sì |
Lazy(T)(Boolean) | True (ExecutionAndPublication) o false (None) |
No | No |
Lazy(T)(Func(T), Boolean) | True (ExecutionAndPublication) o false (None) |
Sì | Sì |
Lazy(T)(LazyThreadSafetyMode) | Specificata dall'utente | No | No |
Lazy(T)(Func(T), LazyThreadSafetyMode) | Specificata dall'utente | Sì | 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
- Nozioni di base sul threading gestito
- Thread e threading
- Task Parallel Library (TPL)
- How to: Perform Lazy Initialization of Objects (Procedura: Eseguire l'inizializzazione differita di oggetti)