Inicialização lenta

Inicialização lenta de um objeto significa que a criação dele é adiada até que ele seja usado pela primeira vez. (Para este tópico, os termos inicialização lenta e instanciação lenta são sinônimos). A inicialização lenta é usada principalmente para melhorar o desempenho, evitar cálculos desnecessários e reduzir os requisitos de memória do programa. Estes são os cenários mais comuns:

  • Quando você tem um objeto de criação dispendiosa e o programa pode não vir a usá-lo. Por exemplo, suponha que você tenha na memória um objeto Customer que tem uma propriedade Orders contendo uma grande variedade de objetos Order que, ao serem inicializados, requerem uma conexão de banco de dados. Se o usuário nunca solicita a exibição das Ordens nem usa os dados em uma computação, não há motivo para usar a memória do sistema ou ciclos de computação para criá-lo. Ao usar Lazy<Orders> para declarar o objeto Orders para inicialização lenta, você pode evitar desperdício de recursos de sistema quando o objeto não é usado.

  • Quando você tem um objeto cuja criação é dispendiosa e você deseja adiar a criação até após a conclusão de outras operações dispendiosas. Por exemplo, suponha que seu programa carrega várias instâncias de objeto quando ele é iniciado, mas apenas algumas delas são necessárias imediatamente. Você pode melhorar o desempenho de inicialização do programa, adiando a inicialização dos objetos que não são necessários até que os objetos necessários tenham sido criados.

Embora você possa escrever seu próprio código para executar a inicialização lenta, recomendamos que você use Lazy<T> em vez disso. Lazy<T> e seus tipos relacionados também dão suporte a segurança do thread e fornecem uma política de propagação de exceção consistente.

A tabela a seguir lista os tipos que o .NET Framework versão 4 fornece para permitir a inicialização lenta em cenários diferentes.

Tipo Descrição
Lazy<T> Uma classe wrapper que fornece a semântica de inicialização lenta para qualquer biblioteca de classes ou tipo definido pelo usuário.
ThreadLocal<T> Ela é semelhante a Lazy<T>, exceto pelo fato de que ela fornece a semântica de inicialização lenta por local de thread. Cada thread tem acesso ao seu próprio valor exclusivo.
LazyInitializer Fornece métodos static avançados (Shared no Visual Basic) para inicialização lenta de objetos sem a sobrecarga de uma classe.

Inicialização lenta básica

Para definir um tipo de inicialização lenta, por exemplo, MyType, use Lazy<MyType> (Lazy(Of MyType) no Visual Basic), conforme mostrado no exemplo a seguir. Se nenhum delegado é passado ao construtor Lazy<T>, o tipo encapsulado é criado usando Activator.CreateInstance quando a propriedade de valor é acessada pela primeira vez. Se o tipo não tiver um construtor sem parâmetros, uma exceção de tempo de execução será lançada.

No exemplo a seguir, suponha que Orders é uma classe que contém uma matriz de objetos Order recuperados de um banco de dados. Um objeto Customer contém uma instância de Orders, mas, dependendo de ações do usuário, os dados do objeto Orders talvez não sejam necessário.

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

Você também pode passar um delegado no construtor Lazy<T> que invoca uma sobrecarga de construtor específica no tipo encapsulado no momento da criação e pode executar outras etapas de inicialização necessárias, conforme mostrado no exemplo a seguir.

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

Depois que o objeto lento é criado, nenhuma instância de Orders é criada até a propriedade Value da variável lenta ser acessada pela primeira vez. No primeiro acesso, o tipo encapsulado é criado e retornado e então é armazenado para eventuais acessos futuros.

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

Um objeto Lazy<T> sempre retorna o mesmo objeto ou valor com o qual ele foi inicializado. Portanto, a propriedade Value é somente leitura. Se Value armazena um tipo de referência, você não pode atribuir um novo objeto a ela. (No entanto, você pode alterar o valor de seus campos e propriedades públicas configuráveis). Se Value armazenar um tipo de valor, você não poderá modificar seu valor. No entanto, você pode criar uma nova variável, invocando o construtor de variável novamente usando novos argumentos.

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

A nova instância lenta, semelhante à anterior, não instanciará Orders até que propriedade Value dele seja acessada pela primeira vez.

Inicialização thread-safe

Por padrão, objetos Lazy<T> são thread-safe. Ou seja, se o construtor não especificar o tipo de acesso thread-safe, os objetos Lazy<T> que ele criar serão thread-safe. Em cenários com vários threads, o primeiro thread para acessar a propriedade Value de um objeto Lazy<T> thread-safe inicializa-o para todos os acessos subsequentes em todos os threads e todos os threads compartilham os mesmos dados. Portanto, não importa qual thread inicializa o objeto e as condições de corrida são benignas.

Observação

Você pode estender essa consistência às condições de erro usando o cache de exceções. Para obter mais informações, consulte a próxima seção, Exceções em objetos lentos.

O exemplo a seguir mostra que a mesma instância Lazy<int> tem o mesmo valor para três threads separados.

// Initialize the integer to the managed thread id of the
// first thread that accesses the Value property.
Lazy<int> number = new Lazy<int>(() => Thread.CurrentThread.ManagedThreadId);

Thread t1 = new Thread(() => Console.WriteLine("number on t1 = {0} ThreadID = {1}",
                                        number.Value, Thread.CurrentThread.ManagedThreadId));
t1.Start();

Thread t2 = new Thread(() => Console.WriteLine("number on t2 = {0} ThreadID = {1}",
                                        number.Value, Thread.CurrentThread.ManagedThreadId));
t2.Start();

Thread t3 = new Thread(() => Console.WriteLine("number on t3 = {0} ThreadID = {1}", number.Value,
                                        Thread.CurrentThread.ManagedThreadId));
t3.Start();

// Ensure that thread IDs are not recycled if the
// first thread completes before the last one starts.
t1.Join();
t2.Join();
t3.Join();

/* Sample Output:
    number on t1 = 11 ThreadID = 11
    number on t3 = 11 ThreadID = 13
    number on t2 = 11 ThreadID = 12
    Press any key to exit.
*/
' Initialize the integer to the managed thread id of the 
' first thread that accesses the Value property.
Dim number As Lazy(Of Integer) = New Lazy(Of Integer)(Function()
                                                          Return Thread.CurrentThread.ManagedThreadId
                                                      End Function)

Dim t1 As New Thread(Sub()
                         Console.WriteLine("number on t1 = {0} threadID = {1}",
                                           number.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
t1.Start()

Dim t2 As New Thread(Sub()
                         Console.WriteLine("number on t2 = {0} threadID = {1}",
                                           number.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
t2.Start()

Dim t3 As New Thread(Sub()
                         Console.WriteLine("number on t3 = {0} threadID = {1}",
                                           number.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
t3.Start()

' Ensure that thread IDs are not recycled if the 
' first thread completes before the last one starts.
t1.Join()
t2.Join()
t3.Join()

' Sample Output:
'       number on t1 = 11 ThreadID = 11
'       number on t3 = 11 ThreadID = 13
'       number on t2 = 11 ThreadID = 12
'       Press any key to exit.

Se você precisar de dados separados em cada thread, use o tipo ThreadLocal<T>, conforme descrito posteriormente neste tópico.

Alguns construtores Lazy<T> têm um parâmetro booliano denominado isThreadSafe que é usado para especificar se a propriedade Value será acessada de vários threads. Se você pretende acessar a propriedade de apenas um thread, passe false para obter uma discreta melhora no desempenho. Se você pretende acessar a propriedade de vários threads, passe true para instruir a instância Lazy<T> para lidar corretamente com condições de corrida em que um thread gera uma exceção em tempo de inicialização.

Alguns construtores Lazy<T> têm um parâmetro LazyThreadSafetyMode chamado mode. Esses construtores fornecem um modo adicional de acesso thread-safe. A tabela a seguir mostra como o acesso thread-safe de um objeto Lazy<T> é afetado pelos parâmetros de construtor que especificam o acesso thread-safe. Cada construtor tem no máximo um desses parâmetros.

Acesso thread-safe do objeto LazyThreadSafetyMode mode parâmetro Parâmetro isThreadSafe booliano Sem parâmetros de acesso thread-safe
Totalmente thread-safe; apenas um thread por vez tenta inicializar o valor. ExecutionAndPublication true Sim.
Não é thread-safe. None false Não aplicável.
Totalmente thread-safe; os threads fazem corrida para inicializar o valor. PublicationOnly Não aplicável. Não aplicável.

Conforme demonstrado na tabela, especificar LazyThreadSafetyMode.ExecutionAndPublication para o parâmetro mode é o mesmo que especificar true para o parâmetro isThreadSafe e especificar LazyThreadSafetyMode.None é o mesmo que especificar false.

Para obter mais informações sobre a que Execution e Publication se referem, consulte LazyThreadSafetyMode.

Especificar LazyThreadSafetyMode.PublicationOnly permite que vários threads tentar inicializar a instância Lazy<T>. Apenas um thread pode vencer essa corrida e todos os outros threads recebem o valor que foi inicializado pelo thread bem-sucedido. Se uma exceção for lançada em um thread durante a inicialização, esse thread não receberá o valor definido pelo thread bem-sucedido. Exceções não são armazenadas em cache, portanto, uma tentativa subsequente de acessar a propriedade Value pode resultar em inicialização bem-sucedida. Isso é diferente da maneira como exceções são tratadas em outros modos, o que é descrito na seção a seguir. Para obter mais informações, consulte a enumeração LazyThreadSafetyMode.

Exceções em objetos lentos

Conforme mencionado anteriormente, um objeto Lazy<T> sempre retorna o mesmo objeto ou valor com o qual ele foi inicializado e, portanto, a propriedade Value é somente leitura. Se você habilitar o cache de exceção, essa imutabilidade também se estenderá ao comportamento de exceção. Se um objeto de inicialização lenta tiver o cache de exceção habilitado e lançar uma exceção de seu método de inicialização quando a propriedade Value for acessada pela primeira vez, essa mesma exceção será lançada em cada tentativa subsequente de acessar a propriedade Value. Em outras palavras, o construtor do tipo encapsulado nunca será invocado novamente, mesmo em cenários com vários threads. Portanto, o objeto Lazy<T> não é capaz de gerar uma exceção em um acesso e retorna um valor em um acesso subsequente.

O cache de exceção é habilitado quando você usar qualquer construtor System.Lazy<T> que usa um método de inicialização (parâmetro valueFactory); por exemplo, ele é habilitado quando você usa o construtor Lazy(T)(Func(T)). Se o construtor também usa um valor LazyThreadSafetyMode (parâmetro mode), especifique LazyThreadSafetyMode.ExecutionAndPublication ou LazyThreadSafetyMode.None. Especificar um método de inicialização permite o cache de exceções para esses dois modos. O método de inicialização pode ser muito simples. Por exemplo, ele pode chamar o construtor sem parâmetros para T: new Lazy<Contents>(() => new Contents(), mode) em C# ou New Lazy(Of Contents)(Function() New Contents()) em Visual Basic. Se você usar um construtor System.Lazy<T> que não especifique um método de inicialização, as exceções geradas pelo construtor sem parâmetros para T não serão armazenadas em cache. Para obter mais informações, consulte a enumeração LazyThreadSafetyMode.

Observação

Se você criar um objeto Lazy<T> com o parâmetro de construtor isThreadSafe definido como false ou o parâmetro de construtor mode definido como LazyThreadSafetyMode.None, você deverá acessar o objeto Lazy<T> de um único thread ou fornecer sua própria sincronização. Isso se aplica a todos os aspectos do objeto, incluindo o cache de exceção.

Conforme observado na seção anterior, objetos Lazy<T> criados com a especificação LazyThreadSafetyMode.PublicationOnly tratam exceções de forma diferente. Com PublicationOnly, vários threads podem competir para inicializar a instância Lazy<T>. Nesse caso, as exceções não são armazenadas em cache e tentativas de acessar a propriedade Value podem continuar até que a inicialização seja bem-sucedida.

A tabela a seguir resume a maneira como os construtores Lazy<T> controlam o cache de exceção.

Construtor Modo de acesso thread-safe Usa o método de inicialização Exceções são armazenadas em cache
Lazy(T)() (ExecutionAndPublication) No No
Lazy(T)(Func(T)) (ExecutionAndPublication) Sim Sim
Lazy(T)(Boolean) True (ExecutionAndPublication) ou false (None) No No
Lazy(T)(Func(T), Boolean) True (ExecutionAndPublication) ou false (None) Sim Sim
Lazy(T)(LazyThreadSafetyMode) Especificado pelo usuário No No
Lazy(T)(Func(T), LazyThreadSafetyMode) Especificado pelo usuário Sim Não se o usuário especifica PublicationOnly; caso contrário, sim.

Implementando uma propriedade de inicialização lenta

Para implementar uma propriedade pública usando inicialização lenta, defina o campo de suporte da propriedade como um Lazy<T> e retorne a propriedade Value do acessador get da propriedade.

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

A propriedade Value é somente leitura; portanto, a propriedade que a expõe não tem nenhum acessador set. Se você precisar de uma propriedade de leitura/gravação com o apoio de um objeto Lazy<T>, o acessador set deverá criar um novo objeto Lazy<T> e atribuí-lo ao repositório de backup. O acessador set deve criar uma expressão lambda que retorna o novo valor da propriedade que foi passado para o acessador set e passar essa expressão lambda para o construtor para o novo objeto Lazy<T>. O próximo acesso à propriedade Value causará a inicialização do novo Lazy<T> e a propriedade Value dele retornará posteriormente o novo valor atribuído à propriedade. A razão para essa organização complicada é preservar as proteções de multithreading como internas em Lazy<T>. Caso contrário, os acessadores de propriedade precisariam armazenar em cache o primeiro valor retornado pela propriedade Value e modificar apenas o valor armazenado em cache e você precisa escrever seu próprio código thread-safe para fazer isso. Devido às inicializações adicionais requeridas por uma propriedade de leitura/gravação com o suporte de um objeto Lazy<T>, o desempenho pode não ser aceitável. Além disso, dependendo do cenário específico, pode ser necessária coordenação adicional para evitar condições de corrida entre setters e getters.

Inicialização lenta de local de thread

Em alguns cenários com vários threads, você talvez queira dar a cada thread seus próprios dados privados. Esses dados são chamados de dados locais de thread. No .NET Framework versão 3.5 e anteriores, você pode aplicar o atributo ThreadStatic a uma variável estática para torná-la local de thread. No entanto, usar o atributo ThreadStatic pode levar a erros. Por exemplo, até mesmo instruções de inicialização básicas farão com que a variável a seja inicializada somente no primeiro thread que a acessar, conforme mostrado no exemplo a seguir.

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

Em todos os outros threads, a variável será inicializada usando seu valor padrão (zero). Como alternativa no .NET Framework versão 4, você pode usar o tipo System.Threading.ThreadLocal<T> para criar uma variável local de thread baseada em instância, que é inicializada em todos os threads pelo delegado Action<T> que você fornece. No exemplo a seguir, todos os threads que acessarem counter verão seu valor inicial como 1.

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

ThreadLocal<T> encapsula seu objeto da mesma maneira que Lazy<T>, com estas diferenças essenciais:

  • Cada thread inicializa a variável local de thread usando seus próprios dados privados que não são acessíveis de outros threads.

  • A propriedade ThreadLocal<T>.Value é de leitura-gravação e pode ser modificada qualquer número de vezes. Isso pode afetar a propagação de exceção, por exemplo, uma operação get pode gerar uma exceção, mas a próxima pode inicializar o valor com êxito.

  • Se nenhum delegado de inicialização for fornecido, ThreadLocal<T> inicializará seu tipo encapsulado usando o valor padrão do tipo. Nesse sentido, ThreadLocal<T> é consistente com o atributo ThreadStaticAttribute.

O exemplo a seguir demonstra que cada thread que acessa a instância de ThreadLocal<int> obtém sua própria cópia exclusiva dos dados.

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

Variáveis locais de thread em Parallel.For e ForEach

Quando você usa o método Parallel.For ou o método Parallel.ForEach para iterar por fontes de dados em paralelo, você pode usar as sobrecargas que têm suporte interno para dados locais de thread. Esses métodos, a localidade do thread é obtida usando delegados locais para criar, acessar e limpar os dados. Para obter mais informações, consulte Como gravar um Loop Parallel.For com variáveis locais de thread e Como gravar um Loop Parallel.ForEach com variáveis locais de partição.

Usando inicialização lenta para cenários de baixa sobrecarga

Em cenários em que você precisa fazer inicialização lenta de um grande número de objetos, você pode decidir que encapsular cada objeto em um Lazy<T> requer memória ou recursos de computação demais. Ou então, você pode ter requisitos rigorosos para como a inicialização lenta é exposta. Nesses casos, você pode usar os métodos static (Shared no Visual Basic) da classe System.Threading.LazyInitializer para fazer inicialização lenta de cada objeto sem encapsulá-lo em uma instância de Lazy<T>.

No exemplo a seguir, suponha que, em vez de encapsular todo um objeto Orders em um objeto Lazy<T>, você inicializou lentamente objetos Order individuais somente se eles são necessários.

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

Neste exemplo, observe que o procedimento de inicialização é invocado em cada iteração do loop. Em cenários com vários threads, o primeiro thread a invocar o procedimento de inicialização é aquele cujo valor é visto por todos os threads. Threads posteriores também chamam o procedimento de inicialização, mas os resultados não são usados. Se esse tipo de condição de corrida potencial não é aceitável, use a sobrecarga de LazyInitializer.EnsureInitialized que usa um argumento booliano e um objeto de sincronização.

Confira também