Поделиться через


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

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

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

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

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

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

Тип

Описание

[ T:System.Lazy`1 ]

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

[ T:System.Threading.ThreadLocal`1 ]

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

[ T:System.Threading.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.
Dim _orders As Lazy(Of Orders) = New Lazy(Of Orders)()
// Initialize by using default Lazy<T> constructor. The 
// Orders array itself is not created yet.
Lazy<Orders> _orders = new Lazy<Orders>();

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

' 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))
// Initialize by invoking a specific constructor on Order when Value
// property is accessed
Lazy<Orders> _orders = new Lazy<Orders>(() => new Orders(100));

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

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

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

_orders = New Lazy(Of Orders)(Function() New Orders(10))
_orders = new Lazy<Orders>(() => 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.
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.
// 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.
*/

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

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

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

Потокобезопасность объекта

Параметр LazyThreadSafetyMode mode

Логический параметр isThreadSafe

Без параметров безопасности потока

Полностью потокобезопасный; только один поток пытается инициализировать значение в определенный момент времени.

[ F:System.Threading.LazyThreadSafetyMode.ExecutionAndPublication ]

true

Да.

Не является потокобезопасным.

[ F:System.Threading.LazyThreadSafetyMode.None ]

false

Неприменимо.

Полностью потокобезопасный; потоки состязаются за право инициализации значения.

[ F:System.Threading.LazyThreadSafetyMode.PublicationOnly ]

Неприменимо.

Неприменимо.

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

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

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

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

Кэширование исключений включено, если используется какой-либо конструктор System.Lazy<T>, использующий метод инициализации (параметр valueFactory); например, оно включено при использовании конструктора Lazy(T)(Func(T)). Если конструктор также принимает значение LazyThreadSafetyMode (параметр mode), укажите LazyThreadSafetyMode.None или LazyThreadSafetyMode.ExecutionAndPublication. Указание метода инициализации включает кэширование исключений для этих двух режимов. Метод инициализации может быть очень простым. Например, он может вызывать конструктор по умолчанию для 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)

Нет

Нет

Lazy(T)(Func(T))

(ExecutionAndPublication)

Да

Да

Lazy(T)(Boolean)

True (ExecutionAndPublication) или false (None)

Нет

Нет

Lazy(T)(Func(T), Boolean)

True (ExecutionAndPublication) или false (None)

Да

Да

Lazy(T)(LazyThreadSafetyMode)

Указывается пользователем

Нет

Нет

Lazy(T)(Func(T), LazyThreadSafetyMode)

Указывается пользователем

Да

Нет, если пользователь задает PublicationOnly; в остальных случаях — да.

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

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

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
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 additonal 
            // initialization steps here.
            return new Orders(this.CustomerID);
        });
    }

    public Orders MyOrders
    {
        get
        {
            // Orders is created on first access here.
            return _orders.Value;
        }
    }
}

Свойство 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()>
Shared counter As Integer
[ThreadStatic]
static int counter = 1;

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

Dim betterCounter As ThreadLocal(Of Integer) = New ThreadLocal(Of Integer)(Function() 1)
ThreadLocal<int> betterCounter = new ThreadLocal<int>(() => 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.
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 
// 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 
*/

Локальные для потока переменные в методах 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 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
// 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);
            });
    }
}

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

См. также

Задачи

Практическое руководство. Неактивная инициализация объектов

Основные понятия

Потоки и работа с потоками

Библиотека параллельных задач

Другие ресурсы

Основы управляемых потоков

Журнал изменений

Дата

Журнал

Причина

Март 2011

Исправлены сведения о кэшировании исключений.

Исправление ошибки содержимого.

Апрель 2011

Дальнейшее изменение сведений о кэшировании исключений.

Исправление ошибки содержимого.

Апрель 2011

Исправление: вызов Lazy<T>.ToString не приводит к инициализации.

Исправление ошибки содержимого.