Sdílet prostřednictvím


Opožděná inicializace

Opožděná inicializace objektu znamená, že jeho vytvoření je odloženo, dokud se poprvé nepoužije. (V tomto tématu jsou termíny opožděné inicializace a opožděné vytváření instancí synonymem.) Opožděná inicializace se primárně používá ke zlepšení výkonu, zabránění plýtvání výpočty a snížení požadavků programu na paměť. Toto jsou nejběžnější scénáře:

  • Pokud máte objekt, který je nákladný k vytvoření a program ho nemusí používat. Předpokládejme například, že máte v paměti Customer objekt, který má Orders vlastnost, která obsahuje velké pole Order objektů, které se mají inicializovat, vyžaduje připojení k databázi. Pokud uživatel nikdy nepožádá o zobrazení objednávek nebo použití dat ve výpočtu, není důvod k jeho vytvoření použít systémové paměti nebo výpočetní cykly. Pomocí deklarování Lazy<Orders> objektu Orders pro opožděnou inicializaci se můžete vyhnout plýtvání systémovými prostředky, když se objekt nepoužívá.

  • Pokud máte objekt, který je nákladný k vytvoření a chcete jeho vytvoření odložit, dokud se nedokončí jiné nákladné operace. Předpokládejme například, že program při spuštění načte několik instancí objektů, ale některé z nich se vyžadují okamžitě. Výkon spuštění programu můžete zlepšit odložením inicializace objektů, které nejsou vyžadovány, dokud nebudou vytvořeny požadované objekty.

I když můžete napsat vlastní kód pro provádění opožděné inicializace, doporučujeme místo toho použít Lazy<T> . Lazy<T> a související typy také podporují bezpečnost vláken a poskytují konzistentní zásady šíření výjimek.

Následující tabulka uvádí typy, které rozhraní .NET Framework verze 4 poskytuje k povolení opožděné inicializace v různých scénářích.

Typ Popis
Lazy<T> Obálková třída, která poskytuje opožděnou inicializaci sémantiku pro libovolnou knihovnu tříd nebo uživatelem definovaný typ.
ThreadLocal<T> Lazy<T> Podobá se s výjimkou toho, že poskytuje opožděnou inicializaci sémantiku v místním vlákně. Každé vlákno má přístup k vlastní jedinečné hodnotě.
LazyInitializer Poskytuje pokročilé static metody (Shared v jazyce Visual Basic) pro opožděné inicializace objektů bez režie třídy.

Základní opožděná inicializace

Chcete-li definovat opožděný inicializovaný typ, MyTypenapříklad , použít Lazy<MyType> (Lazy(Of MyType) v jazyce Visual Basic), jak je znázorněno v následujícím příkladu. Pokud není v konstruktoru Lazy<T> předán žádný delegát, je zabalený typ vytvořen pomocí Activator.CreateInstance při prvním přístupu k hodnotové vlastnosti. Pokud typ nemá konstruktor bez parametrů, vyvolá se výjimka za běhu.

V následujícím příkladu předpokládejme, že Orders je třída, která obsahuje pole Order objektů načtených z databáze. Objekt Customer obsahuje instanci objektu Orders, ale v závislosti na akcích uživatele nemusí být data z objektu Orders nutná.

// 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)()

Můžete také předat delegáta v Lazy<T> konstruktoru, který vyvolá konkrétní přetížení konstruktoru na zabalený typ v době vytvoření a provést všechny další požadované inicializační kroky, jak je znázorněno v následujícím příkladu.

// 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))

Po vytvoření lazy objektu se nevytvořila žádná instance Orders , dokud Value se k vlastnosti Lazy proměnné poprvé nepřistupuje. Při prvním přístupu se vytvoří a vrátí zabalený typ a uloží se pro jakýkoli budoucí přístup.

// 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

Objekt Lazy<T> vždy vrátí stejný objekt nebo hodnotu, se kterou byl inicializován. Proto je vlastnost určena jen pro Value čtení. Pokud Value uložíte typ odkazu, nemůžete mu přiřadit nový objekt. (Můžete ale změnit hodnotu jeho nastavených veřejných polí a vlastností.) Pokud Value uložíte typ hodnoty, nemůžete jeho hodnotu změnit. Novou proměnnou však můžete vytvořit opětovným vyvoláním konstruktoru proměnné pomocí nových argumentů.

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

Nová opožděná instance, podobně jako předchozí instance, se neskutečňuje Orders , dokud se k její Value vlastnosti poprvé nepřistupuje.

Inicializace Sejf vláken

Ve výchozím nastavení Lazy<T> jsou objekty bezpečné pro přístup z více vláken. To znamená, že pokud konstruktor neurčí druh zabezpečení vlákna, objekty, které vytvoří, Lazy<T> jsou bezpečné pro přístup z více vláken. Ve scénářích s více vlákny první vlákno pro přístup k Value vlastnosti objektu bezpečného Lazy<T> pro přístup z více vláken inicializuje pro všechny následné přístupy ve všech vláknech a všechna vlákna sdílejí stejná data. Proto nezáleží na tom, který vlákno inicializuje objekt, a časování podmínky jsou neškodné.

Poznámka:

Tuto konzistenci můžete rozšířit na chybové stavy pomocí ukládání výjimek do mezipaměti. Další informace najdete v další části Výjimky v opožděných objektech.

Následující příklad ukazuje, že stejná Lazy<int> instance má stejnou hodnotu pro tři samostatná vlákna.

// 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.

Pokud pro každé vlákno požadujete samostatná data, použijte ThreadLocal<T> typ, jak je popsáno dále v tomto tématu.

Některé Lazy<T> konstruktory mají logický parametr s názvem isThreadSafe , který se používá k určení, zda Value bude vlastnost přístupná z více vláken. Pokud máte v úmyslu získat přístup k vlastnosti pouze z jednoho vlákna, předejte false , abyste získali skromnou výhodu výkonu. Pokud máte v úmyslu přistupovat k vlastnosti z více vláken, předejte true instanci, Lazy<T> aby správně zvládla časování podmínek, ve kterých jedno vlákno vyvolá výjimku v době inicializace.

Některé Lazy<T> konstruktory mají LazyThreadSafetyMode parametr s názvem mode. Tyto konstruktory poskytují další bezpečnostní režim vlákna. Následující tabulka ukazuje, jak je zabezpečení vlákna objektu Lazy<T> ovlivněno parametry konstruktoru, které určují zabezpečení vlákna. Každý konstruktor má maximálně jeden takový parametr.

Zabezpečení vlákna objektu LazyThreadSafetyModemode Parametr isThreadSafe Logický parametr Žádné parametry zabezpečení vláken
Plně bezpečné pro přístup z více vláken; Pouze jedno vlákno se pokusí inicializovat hodnotu. ExecutionAndPublication true Ano.
Není bezpečné pro přístup z více vláken. None false Nevztahuje se.
Plně bezpečné pro přístup z více vláken; threads race to initialize the value. PublicationOnly Nevztahuje se. Nevztahuje se.

Jak ukazuje tabulka, určení parametru mode je stejné jako určení true parametru isThreadSafe a určení LazyThreadSafetyMode.None je stejné jako určení false.LazyThreadSafetyMode.ExecutionAndPublication

Další informace o tom, co Execution a Publication odkazovat, naleznete v tématu LazyThreadSafetyMode.

Určení LazyThreadSafetyMode.PublicationOnly umožňuje, aby se více vláken pokusilo inicializovat Lazy<T> instanci. Tuto rasu může vyhrát pouze jedno vlákno a všechna ostatní vlákna obdrží hodnotu, která byla inicializována úspěšným vláknem. Pokud je při inicializaci vyvolán výjimka ve vlákně, toto vlákno neobdrží hodnotu nastavenou úspěšným vláknem. Výjimky nejsou uloženy v mezipaměti, takže následný pokus o přístup Value k vlastnosti může vést k úspěšné inicializaci. Liší se od způsobu zpracování výjimek v jiných režimech, které jsou popsány v následující části. Další informace naleznete v výčtu LazyThreadSafetyMode .

Výjimky v opožděných objektech

Jak bylo uvedeno dříve, Lazy<T> objekt vždy vrátí stejný objekt nebo hodnotu, se kterou byl inicializován, a proto Value je vlastnost jen pro čtení. Pokud povolíte ukládání výjimek do mezipaměti, tato neměnnost se také rozšíří na chování výjimek. Pokud má opožděný inicializovaný objekt povoleno ukládání výjimek do mezipaměti a vyvolá výjimku z jeho inicializační metody při Value prvním přístupu k vlastnosti, tato stejná výjimka je vyvolána při každém následném pokusu Value o přístup k vlastnosti. Jinými slovy, konstruktor zabaleného typu se nikdy znovu nevyvolá, a to ani ve scénářích s více vlákny. Lazy<T> Objekt proto nemůže vyvolat výjimku pro jeden přístup a vrátit hodnotu pro následný přístup.

Ukládání výjimek do mezipaměti je povoleno při použití libovolného System.Lazy<T> konstruktoru, který přebírá inicializační metodu (valueFactory parametr), například je povolena při použití konstruktoru Lazy(T)(Func(T)). Pokud konstruktor také přebírá LazyThreadSafetyMode hodnotu (mode parametr), zadejte LazyThreadSafetyMode.ExecutionAndPublication nebo LazyThreadSafetyMode.None. Zadání metody inicializace umožňuje ukládání výjimek do mezipaměti pro tyto dva režimy. Metoda inicializace může být velmi jednoduchá. Může například volat konstruktor bez parametrů pro T: new Lazy<Contents>(() => new Contents(), mode) v jazyce C# nebo New Lazy(Of Contents)(Function() New Contents()) v jazyce Visual Basic. Pokud použijete System.Lazy<T> konstruktor, který neurčí inicializační metodu, výjimky vyvolané konstruktorem bez parametrů nejsou T uloženy do mezipaměti. Další informace naleznete v výčtu LazyThreadSafetyMode .

Poznámka:

Pokud vytvoříte Lazy<T> objekt s parametrem konstruktoru isThreadSafe nastaveným na false nebo mode parametrem konstruktoru nastaveným na LazyThreadSafetyMode.None, musíte k objektu přistupovat Lazy<T> z jednoho vlákna nebo poskytnout vlastní synchronizaci. To platí pro všechny aspekty objektu, včetně ukládání výjimek do mezipaměti.

Jak je uvedeno v předchozí části, Lazy<T> objekty vytvořené zadáním zacházet s LazyThreadSafetyMode.PublicationOnly výjimkami odlišně. S PublicationOnly, více vláken může soutěžit o inicializaci Lazy<T> instance. V tomto případě se výjimky neukládají do mezipaměti a pokusy o přístup Value k vlastnosti mohou pokračovat, dokud nebude inicializace úspěšná.

Následující tabulka shrnuje způsob, jakým Lazy<T> konstruktory řídí ukládání výjimek do mezipaměti.

Konstruktor Režim zabezpečení vláken Používá inicializační metodu. Výjimky se ukládají do mezipaměti.
Lazy(T)() (ExecutionAndPublication) No Ne
Lazy(T)(Func(T)) (ExecutionAndPublication) Ano Yes
Lazy(T)(Boolean) True (ExecutionAndPublication) nebo false (None) No Ne
Lazy(T)(Func(T), Logická hodnota) True (ExecutionAndPublication) nebo false (None) Ano Yes
Lazy(T)(LazyThread Sejf tyMode) Zadané uživatelem No Ne
Lazy(T)(Func(T), LazyThread Sejf tyMode) Zadané uživatelem Ano Ne, pokud uživatel určuje PublicationOnly; jinak ano.

Implementace opožděné inicializované vlastnosti

Chcete-li implementovat veřejnou vlastnost pomocí opožděné inicializace, definujte backing pole vlastnosti jako Lazy<T>a vraťte Value vlastnost z get příslušenství vlastnosti.

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

Vlastnost Value je určena jen pro čtení, a proto vlastnost, která ji zveřejňuje, nemá žádné set příslušenství. Pokud požadujete vlastnost pro čtení a zápis zálohovanou objektem Lazy<T> , set musí objekt vytvořit nový Lazy<T> objekt a přiřadit ho k záložnímu úložišti. Přistupovací set objekt musí vytvořit výraz lambda, který vrátí novou hodnotu vlastnosti, která byla předána do přístupového objektu set , a předat tento výraz lambda konstruktoru pro nový Lazy<T> objekt. Další přístup vlastnosti Value způsobí inicializaci nového Lazy<T>a jeho Value vlastnost vrátí novou hodnotu, která byla přiřazena k vlastnosti. Důvodem tohoto konvolutovaného uspořádání je zachování vícevláknových ochrany integrovaných do Lazy<T>. Jinak by přístupové objekty vlastností musely ukládat první hodnotu vrácenou Value vlastností do mezipaměti a upravovat pouze hodnotu uloženou v mezipaměti a k tomu byste museli napsat vlastní kód bezpečný pro vlákno. Vzhledem k dalším inicializacím vyžadovaným vlastností pro čtení a zápis zálohované objektem Lazy<T> nemusí být výkon přijatelný. V závislosti na konkrétním scénáři se navíc může vyžadovat další koordinace, aby nedocházelo k podmínkám časování mezi settery a gettery.

Opožděná inicializace z více vláken

V některých scénářích s více vlákny můžete chtít dát každému vláknu vlastní privátní data. Taková data se nazývají místní data vlákna. V rozhraní .NET Framework verze 3.5 a starší můžete ThreadStatic použít atribut na statickou proměnnou, aby byla místní pro vlákno. Použití atributu ThreadStatic ale může vést k drobným chybám. Například i základní inicializační příkazy způsobí inicializaci proměnné pouze v prvním vlákně, které k němu přistupuje, jak je znázorněno v následujícím příkladu.

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

Ve všech ostatních vláknech bude proměnná inicializována pomocí výchozí hodnoty (nula). Jako alternativu v rozhraní .NET Framework verze 4 můžete pomocí System.Threading.ThreadLocal<T> typu vytvořit místní proměnnou založenou na instanci, která je inicializována ve všech vláknech delegátem Action<T> , který zadáte. V následujícím příkladu uvidí všechna vlákna, která přistupují counter , počáteční hodnotu 1.

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

ThreadLocal<T> zabalí objekt velmi stejným způsobem jako Lazy<T>, s těmito zásadními rozdíly:

  • Každé vlákno inicializuje místní proměnnou vlákna pomocí vlastních privátních dat, která nejsou přístupná z jiných vláken.

  • Vlastnost ThreadLocal<T>.Value je pro čtení i zápis a je možné ji změnit libovolný počet. To může ovlivnit šíření výjimek, například jedna get operace může vyvolat výjimku, ale další může úspěšně inicializovat hodnotu.

  • Pokud není zadán žádný inicializační delegát, ThreadLocal<T> inicializuje jeho zabalený typ pomocí výchozí hodnoty typu. V tomto ohledu ThreadLocal<T> je v souladu s atributem ThreadStaticAttribute .

Následující příklad ukazuje, že každé vlákno, které přistupuje k ThreadLocal<int> instanci získá vlastní jedinečnou kopii dat.

// 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 

Místní proměnné vláken paralelně.For a ForEach

Pokud metodu nebo Parallel.ForEach metodu Parallel.For používáte k paralelní iteraci nad zdroji dat, můžete použít přetížení, která mají integrovanou podporu pro místní data vláken. V těchto metodách je umístění vlákna dosaženo pomocí místních delegátů k vytvoření, přístupu a vyčištění dat. Další informace najdete v tématu Postupy: Zápis smyčky Parallel.For s využitím místních proměnných vláken a postupy: Zápis smyčky Parallel.ForEach pomocí místních proměnných oddílů.

Použití opožděné inicializace pro scénáře s nízkou režií

V situacích, kdy potřebujete opožděně inicializovat velký počet objektů, se můžete rozhodnout, že zabalení každého objektu Lazy<T> do příliš velké paměti nebo příliš mnoho výpočetních prostředků. Nebo můžete mít přísné požadavky na zpřístupnění opožděné inicializace. V takových případech můžete použít static (Shared v jazyce Visual Basic) metody System.Threading.LazyInitializer třídy opožděné inicializace každého objektu bez zabalení v instanci Lazy<T>.

V následujícím příkladu předpokládejme, že namísto zabalení celého Orders objektu do jednoho Lazy<T> objektu máte opožděné inicializované jednotlivé Order objekty pouze v případě, že jsou požadovány.

// 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

V tomto příkladu si všimněte, že inicializační procedura je vyvolána při každé iteraci smyčky. Ve scénářích s více vlákny je prvním vláknem vyvolání inicializační procedury ten, jehož hodnota je zobrazena všemi vlákny. Později vlákna také vyvolat inicializační proceduru, ale jejich výsledky se nepoužívají. Pokud tento druh potenciální časování není přijatelný, použijte přetížení LazyInitializer.EnsureInitialized , který přebírá logický argument a synchronizační objekt.

Viz také