Отложенная инициализация

Отложенная инициализация объекта означает, что его создание откладывается до первого использования. (В этом разделе термины ленивой инициализации и ленивые экземпляры являются синонимами.) Ленивая инициализация в основном используется для повышения производительности, предотвращения расточительного вычисления и уменьшения требований к памяти программы. Ниже приведены наиболее распространенные сценарии.

  • При наличии объекта, создание которого требует много ресурсов и который, возможно, не будет использоваться программой. Например, предположим, что в памяти находится объект Customer, у которого есть свойство Orders, содержащее большой массив объектов Order, инициализация которых требует подключения к базе данных. Если пользователь никогда не отображает массив Orders и не использует его данные в расчетах, то нет смысла использовать системную память или такты процессора для создания этого массива. Используя Lazy<Orders>, чтобы объявить отложенную инициализацию объекта Orders, можно избежать расхода системных ресурсов на неиспользуемый объект.

  • При наличии объекта, требующее много ресурсов создание которого желательно отложить до завершения других ресурсоемких операций. Например, пусть программа во время запуска загружает несколько экземпляров объекта, но только часть из них требуется сразу. Можно повысить быстродействие программы при запуске, отложив инициализацию временно ненужных объектов до того, как они понадобятся.

Хотя вы можете написать свой код для выполнения отложенной инициализации, вместо этого рекомендуется использовать тип Lazy<T>. Тип Lazy<T> и связанные с ним типы также поддерживают безопасность потоков и обеспечивают согласованную политику распространения исключений.

В таблице ниже приведены типы, предоставляемые в .NET Framework версии 4 для поддержки отложенной инициализации в различных сценариях.

Тип Описание
Lazy<T> Класс-оболочка, предоставляющий семантику отложенной инициализации для любого типа из библиотеки классов или пользовательского типа.
ThreadLocal<T> Похож на тип Lazy<T> за исключением того, что предоставляет семантику отложенной инициализации на основе локального потока. У каждого потока есть доступ к собственному уникальному значению.
LazyInitializer Предоставляет расширенные методы static (Shared в Visual Basic) для отложенной инициализации объектов без дополнительных издержек для класса.

Базовая отложенная инициализация

Чтобы определить тип с отложенной инициализацией, например MyType, используйте Lazy<MyType> (Lazy(Of MyType) в Visual Basic), как показано в следующем примере. Если в конструктор Lazy<T> не передается делегат, при первом доступе к свойству значения заключенный в оболочку тип создается с помощью Activator.CreateInstance. Если тип не имеет конструктора без параметров, создается исключение во время выполнения.

В следующем примере предполагается, что Orders — это класс, содержащий массив объектов Order, извлекаемых из базы данных. Объект Customer содержит экземпляр Orders, но в зависимости от действий пользователя данные из объекта Orders могут и не понадобиться.

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

Можно также передать делегат в конструктор Lazy<T>, вызывающий во время создания особую перегруженную версию конструктора для заключенного в оболочку типа, и выполнить любые другие действия, необходимые для инициализации, как показано в следующем примере.

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

После создания объекта Lazy экземпляр Orders не создается до первого доступа к свойству Value переменной Lazy. При первом доступе заключенный в оболочку тип создается, возвращается и сохраняется для любого использования в будущем.

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

Объект Lazy<T> всегда возвращает тот же объект или то же значение, которые использовались для его инициализации. Следовательно, свойство Value доступно только для чтения. Если в свойстве Value хранится ссылочный тип, нельзя присвоить этому свойству новый объект. (Однако можно изменить значение его настраиваемых общедоступных полей и свойств.) Если Value сохраняет тип значения, его значение изменить нельзя. Тем не менее можно создать новую переменную, вызвав конструктор переменной еще раз с новыми аргументами.

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

Новый экземпляр с отложенной инициализацией, как и предыдущий, не будет инициализировать Orders до первого обращения к его свойству Value.

Потокобезопасная инициализация

По умолчанию объекты Lazy<T> являются потокобезопасными. То есть если конструктор не задает тип потокобезопасности, создаваемые им объекты Lazy<T> являются потокобезопасными. В сценариях с несколькими потоками первый поток, обращающийся к свойству Value потокобезопасного объекта Lazy<T>, инициализирует его для всех последующих случаев доступа из всех потоков, и все потоки совместно используют одни и те же данные. Следовательно, неважно, какой поток инициализирует объект, и состояния гонки являются мягкими.

Примечание.

Можно повысить эту устойчивость к ошибкам путем кэширования исключений. Дополнительные сведения см. в следующем разделе Исключения в объектах с отложенной инициализацией.

В приведенном ниже примере показано, что один и тот же экземпляр Lazy<int> обладает одним и тем же значением для трех отдельных потоков.

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

Если для каждого потока требуются собственные данные, используйте тип ThreadLocal<T>, описанный в этом разделе ранее.

Некоторые конструкторы Lazy<T> обладают логическим параметром с именем isThreadSafe, используемым для определения того, будет ли свойство Value доступно из нескольких потоков. Если предполагается, что свойство будет доступно только из одного потока, передайте значение false, чтобы получить небольшой выигрыш в быстродействии. Если предполагается доступ к свойству из нескольких потоков, передайте значение true, чтобы указать экземпляру Lazy<T> на необходимость правильно обрабатывать состояния гонки, в которых один поток создает исключение во время инициализации.

Некоторые конструкторы Lazy<T> имеют параметр LazyThreadSafetyMode с именем mode. Эти конструкторы предоставляют дополнительный режим потокобезопасности. В приведенной ниже таблице показано, как на потокобезопасность объекта Lazy<T> влияют параметры конструктора, задающие потокобезопасность. Каждый конструктор имеет не более одного такого параметра.

Потокобезопасность объекта LazyThreadSafetyModemode параметр Логический параметр isThreadSafe Без параметров потокобезопасности
Полностью потокобезопасный; только один поток пытается инициализировать значение в определенный момент времени. ExecutionAndPublication true Да.
Не является потокобезопасным. None false Неприменимо.
Полностью потокобезопасный; потоки состязаются за право инициализации значения. PublicationOnly Неприменимо. Неприменимо.

Как показано в таблице, указание LazyThreadSafetyMode.ExecutionAndPublication для параметра mode равносильно указанию true для параметра isThreadSafe, а указание LazyThreadSafetyMode.None равносильно указанию false.

Дополнительные сведения о том, что Execution и Publication см. в статье LazyThreadSafetyMode.

При указании LazyThreadSafetyMode.PublicationOnly допускается, чтобы несколько потоков пытались инициализировать экземпляр Lazy<T>. Только один поток может выиграть это состязание, и все другие потоки получают значение, которое было инициализировано успешным потоком. Если во время инициализации в потоке создается исключение, этот поток не получает значение, установленное успешным потоком. Исключения не кэшируются, поэтому повторная попытка доступа к свойству Value может привести к успешной инициализации. Это отличается от способа обработки исключений в других режимах, которые описаны в следующем разделе. Дополнительные сведения см. в описании перечисления LazyThreadSafetyMode.

Исключения в объектах с отложенной инициализацией

Как упоминалось выше, объект Lazy<T> всегда возвращает тот же объект или то же значение, которые использовались для его инициализации, следовательно, свойство Value доступно только для чтения. Если включено кэширование исключений, эта неизменность также распространяется на поведение исключений. Если у отложенного инициализированного объекта включена кэширование исключений и создается исключение из метода инициализации при Value первом доступе к свойству, то это же исключение возникает при каждой последующей попытке доступа к Value свойству. Другими словами, конструктор заключенного в оболочку типа никогда не вызывается повторно даже в сценариях с несколькими потоками. Следовательно, объект Lazy<T> не может создавать исключение при одной попытке доступа и возвращать значение при последующих попытках доступа.

Кэширование исключений включено, если используется какой-либо конструктор System.Lazy<T>, принимающий метод инициализации (параметр valueFactory); например, оно включено при использовании конструктора Lazy(T)(Func(T)). Если конструктор также принимает значение LazyThreadSafetyMode (параметр mode), укажите LazyThreadSafetyMode.ExecutionAndPublication или LazyThreadSafetyMode.None. Указание метода инициализации включает кэширование исключений для этих двух режимов. Метод инициализации может быть очень простым. Например, он может вызывать конструктор без параметров для T: new Lazy<Contents>(() => new Contents(), mode) в C#или New Lazy(Of Contents)(Function() New Contents()) в Visual Basic. Если вы используете конструктор System.Lazy<T>, который не указывает метод инициализации, исключения, вызываемые конструктором без параметров для T, не кэшируются. Дополнительные сведения см. в описании перечисления LazyThreadSafetyMode.

Примечание.

Если создается объект Lazy<T> с параметром конструктора isThreadSafe, установленным в значение false, или с параметром конструктора mode, установленным в значение LazyThreadSafetyMode.None, обращаться к объекту Lazy<T> необходимо из одного потока или обеспечить свою собственную синхронизацию. Это относится ко всем аспектам объекта, включая кэширование исключений.

Как отмечалось в предыдущем разделе, объекты Lazy<T>, созданные путем указания LazyThreadSafetyMode.PublicationOnly, обрабатывают исключения иначе. В случае с PublicationOnly за инициализацию экземпляра Lazy<T> могут конкурировать несколько потоков. В этом случае исключения не кэшируются, и попытки доступа к свойству Value могут продолжаться до успешной инициализации.

В приведенной ниже таблице описывается способ, которым конструкторы Lazy<T> управляют кэшированием исключений.

Конструктор Потокобезопасный режим Использует метод инициализации Исключения кэшируются
Lazy(T)() (ExecutionAndPublication) No No
Lazy(T)(Func(T)) (ExecutionAndPublication) Да Да
Lazy(T)(Boolean) True (ExecutionAndPublication) или false (None) No No
Lazy(T)(Func(T), Boolean) True (ExecutionAndPublication) или false (None) Да Да
Lazy(T)(LazyThreadSafetyMode) Указывается пользователем No No
Lazy(T)(Func(T), LazyThreadSafetyMode) Указывается пользователем Да Нет, если пользователь задает PublicationOnly; в остальных случаях — да.

Реализация свойства с отложенной инициализацией

Для реализации открытого свойства с помощью отложенной инициализации определите резервное поле свойства как Lazy<T> и верните свойство Value из метода доступа get этого свойства.

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

Свойство Value доступно только для чтения, следовательно, у свойства, предоставляющего его, нет метода доступа set. Если требуется свойство для чтения и записи, резервируемое объектом Lazy<T>, метод доступа set должен создать объект Lazy<T> и присвоить его резервному хранилищу. Метод доступа set должен создать лямбда-выражение, возвращающее новое значение свойства, которое было передано методу доступа set, и передать это лямбда-выражение конструктору нового объекта Lazy<T>. Следующее обращение к свойству Value вызовет инициализацию нового объекта Lazy<T>, и его свойство Value после этого будет возвращать новое значение, присвоенное этому свойству. Эта сложная структура требуется для сохранения средств защиты многопоточности, встроенных в Lazy<T>. В противном случае методам доступа к свойству пришлось бы кэшировать первое значение, возвращаемое свойством Value, и изменять только кэшированное значение, а вам пришлось бы писать для этого собственный потокобезопасный код. Из-за дополнительных инициализаций, необходимых для свойства чтения и записи, резервируемого объектом Lazy<T>, производительность может стать неприемлемой. Более того, в зависимости от конкретного сценария может потребоваться дополнительная координация, чтобы избежать состояний гонки между методами задания и получения значений.

Локальная по отношению к потоку отложенная инициализация

В некоторых сценариях с несколькими потоками может понадобиться, чтобы каждый поток работал со своими собственными закрытыми данными. Такие данные называются локальными по отношению к потоку. В .NET Framework версии 3.5 и предыдущих версий можно было применить к статической переменной атрибут ThreadStatic, чтобы сделать ее локальной по отношению к потоку. Однако использование атрибута ThreadStatic может привести к скрытым ошибкам. Например, даже основные операторы инициализации могут привести к инициализации переменной только для первого обратившегося к ней потока, как показано в приведенном ниже примере.

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

Во всех остальных потоках переменная будет инициализирована своим значением по умолчанию (нулем). В качестве альтернативы в .NET Framework версии 4 можно использовать тип System.Threading.ThreadLocal<T>, чтобы создать локальную по отношению к потоку переменную на основе экземпляра, инициализируемую во всех потоках с помощью предоставленного вами делегата Action<T>. В приведенном ниже примере во всех потоках, обращающихся к счетчику counter, начальное значение этого счетчика будет равно 1.

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

ThreadLocal<T> заключает свой объект в оболочку в основном так же, как и Lazy<T>, с перечисленными ниже важными различиями.

  • Каждый поток инициализирует локальную по отношению к потоку переменную, используя собственные закрытые данные, недоступные другим потокам.

  • Свойство ThreadLocal<T>.Value доступно для чтения и записи и может быть изменено любое количество раз. Это может повлиять на распространение исключений. Так, одна операция get может создать исключение, а следующая — успешно инициализировать значение.

  • Если делегат для инициализации не предоставлен, ThreadLocal<T> будет инициализировать соответствующий заключенный в оболочку тип, используя значение по умолчанию для этого типа. В этом отношении ThreadLocal<T> согласуется с атрибутом ThreadStaticAttribute.

В приведенном ниже примере показано, как каждый поток, обращающийся к экземпляру ThreadLocal<int>, получает собственную уникальную копию данных.

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

Локальные по отношению к потоку переменные в методах Parallel.For и ForEach

При использовании метода Parallel.For или метода Parallel.ForEach для параллельной итерации источников данных можно применить перегруженные версии со встроенной поддержкой локальных по отношению к потоку данных. В этих методах локальность по отношению к потоку достигается с помощью локальных делегатов, используемых для создания данных, доступа к ним и их очистки. Дополнительные сведения см. в разделах Практическое руководство. Написание цикла Parallel.For с локальными переменными потока и Практическое руководство. Написание цикла Parallel.ForEach с локальными переменными раздела.

Использование отложенной инициализации для сценариев с низкими издержками

Если необходимо использовать отложенную инициализацию для большого числа объектов, может оказаться, что заключение каждого объекта в оболочку Lazy<T> требует слишком много памяти или вычислительных ресурсов. Либо могут предъявляться строгие требования к предоставлению отложенной инициализации. В таких случаях можно использовать методы static (Shared в Visual Basic) класса System.Threading.LazyInitializer, чтобы выполнить отложенную инициализацию каждого объекта, не заключая его в экземпляр Lazy<T>.

В приведенном ниже примере предполагается, что вместо заключения в оболочку всего объекта Orders в одном объекте Lazy<T> выполняется отложенная инициализация отдельных объектов Order только в случае их необходимости.

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

В этом примере обратите внимание на то, что процедура инициализации вызывается для каждой итерации цикла. В сценариях с несколькими потоками первый поток, вызывающий процедуру инициализации, определяет значение, которое будет доступно всем потокам. Последующие потоки также вызывают процедуру инициализации, но их результаты не используются. Если этот вид возможного состояния гонки является недопустимым, используйте перегруженную версию типа LazyInitializer.EnsureInitialized, которой передается логический аргумент и объект синхронизации.

См. также