Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
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 istanza differita sono sinonimi. L'inizializzazione differita viene principalmente utilizzata 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 sicurezza dei thread e forniscono una politica di propagazione uniforme delle eccezioni.
La tabella seguente elenca i tipi che il .NET Framework versione 4 fornisce per abilitare l'inizializzazione lenta in diversi scenari.
Tipo | Descrizione |
---|---|
Lazy<T> | La classe wrapper che fornisce una semantica di inizializzazione ritardata per una qualsiasi libreria di classi o per un 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 con inizializzazione differita, ad esempio MyType
, si può usare Lazy<MyType>
(Lazy(Of MyType)
in Visual Basic), come mostrato nell'esempio seguente. Se non viene passata alcuna funzione delegata nel costruttore Lazy<T>, il tipo con wrapping viene creato usando Activator.CreateInstance al primo accesso alla proprietà 'value'. 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();
' 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 uno specifico overload del costruttore nel tipo incapsulato al momento della creazione, ed eseguire tutti gli altri passaggi di inizializzazione necessari, come dimostrato nell'esempio seguente.
// Initialize by invoking a specific constructor on Order when Value
// property is accessed
Lazy<Orders> _orders = new(() => 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 racchiuso viene creato, restituito e quindi memorizzato per i futuri accessi.
// We need to create the array only if displayOrders is true
if (s_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 quella precedente, non crea un'istanza di Orders
fino al primo accesso alla proprietà Value.
Inizializzazione thread-safe
Per impostazione predefinita, gli oggetti Lazy<T> sono thread-safe. Vale a dire, se il costruttore non specifica il tipo di sicurezza del thread, gli oggetti Lazy<T> che crea sono sicuri per il thread. 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 innocue.
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(() => Environment.CurrentManagedThreadId);
Thread t1 = new(() => Console.WriteLine($"number on t1 = {number.Value} ThreadID = {Environment.CurrentManagedThreadId}"));
t1.Start();
Thread t2 = new(() => Console.WriteLine($"number on t2 = {number.Value} ThreadID = {Environment.CurrentManagedThreadId}"));
t2.Start();
Thread t3 = new(() => Console.WriteLine($"number on t3 = {number.Value} ThreadID = {Environment.CurrentManagedThreadId}"));
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, inserire il valore 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 sicurezza dei thread aggiuntiva. La tabella seguente mostra come i parametri del costruttore che specificano la sicurezza dei thread influiscano su 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 sicurezza dei thread |
---|---|---|---|
Completamente thread-safe. Un solo thread alla volta tenta di inizializzare il valore. | ExecutionAndPublication | true |
Sì. |
Non sicuro per i thread. | None | false |
Non applicabile. |
Completamente thread-safe: i thread si contendono l'inizializzazione del 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ò vincere questa corsa, e tutti gli altri thread ricevono il valore che è stato inizializzato dal thread vincente. 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à pigramente inizializzata
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 readonly 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(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 questa disposizione complessa è 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 s_counter = 1;
<ThreadStatic()>
Shared counter As Integer
In tutti gli altri thread la variabile verrà inizializzata usando il valore predefinito (zero). Come alternativa, nella versione 4 del .NET Framework è possibile usare il tipo System.Threading.ThreadLocal<T> per creare una variabile locale al thread basata su istanza che viene inizializzata su tutti i thread dal delegato Action<T> che fornisci. Nell'esempio seguente tutti i thread che accedono a counter
visualizzeranno il valore iniziale come 1.
ThreadLocal<int> _betterCounter = new(() => 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 viene fornito alcun delegato di inizializzazione, ThreadLocal<T> inizializzerà il tipo incapsulato utilizzando 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(() => Environment.CurrentManagedThreadId);
Thread t4 = new(() => Console.WriteLine($"threadLocalNumber on t4 = {threadLocalNumber.Value} ThreadID = {Environment.CurrentManagedThreadId}"));
t4.Start();
Thread t5 = new(() => Console.WriteLine($"threadLocalNumber on t5 = {threadLocalNumber.Value} ThreadID = {Environment.CurrentManagedThreadId}"));
t5.Start();
Thread t6 = new(() => Console.WriteLine($"threadLocalNumber on t6 = {threadLocalNumber.Value} ThreadID = {Environment.CurrentManagedThreadId}"));
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 iterare sulle origini dati in parallelo, è possibile usare gli overload che includono il supporto integrato per i 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 decidere che l'inserimento di ciascun oggetto in un 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 ritardo ogni oggetto senza racchiuderli 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 un'eventuale race condition di questo tipo non è accettabile, utilizzare la versione sovraccarica 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)