Partilhar via


Inicialização lenta

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

  • Quando você tem um objeto que é caro para criar, e o programa pode não usá-lo. Por exemplo, suponha que você tenha na memória um Customer objeto que tenha uma Orders propriedade que contenha uma grande matriz de objetos que, para ser inicializado, requer uma conexão de banco de Order dados. Se o utilizador nunca solicitar a exibição das encomendas ou usar os dados num cálculo, então não há razão para usar a memória do sistema ou ciclos de processamento para o criar. Lazy<Orders> Usando para declarar o objeto para inicialização lentaOrders, você pode evitar o desperdício de recursos do sistema quando o objeto não é usado.

  • Quando você tem um objeto que é caro para criar e você deseja adiar sua criação até depois de outras operações caras terem sido concluídas. Por exemplo, suponha que seu programa carrega várias instâncias de objeto quando é 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 oferecem suporte à segurança de threads e fornecem uma política consistente de propagação de exceções.

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

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

Inicialização preguiçosa básica

Para definir um tipo de inicialização lenta, por exemplo, MyTypeuse Lazy<MyType> (Lazy(Of MyType) no Visual Basic), conforme mostrado no exemplo a seguir. Se nenhum delegado for passado no Lazy<T> construtor, o tipo envolto será criado usando Activator.CreateInstance quando a propriedade valor for acedida 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 recuperados de um banco de Order dados. Um objeto Customer contém uma instância de Orders, mas dependendo das ações do usuário, os dados do objeto Orders podem não ser necessários.

// Initialize by using default Lazy<T> constructor. The
// Orders array itself is not created yet.
Lazy<Orders> _orders = new();
' Initialize by using default Lazy<T> constructor. The 
'Orders array itself is not created yet.
Dim _orders As Lazy(Of Orders) = New Lazy(Of Orders)()

Você pode também passar uma função delegada no construtor Lazy<T> que invoca uma sobrecarga de construtor específica no tipo encapsulado no momento da sua criação e executar quaisquer 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(() => 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 Lazy é criado, nenhuma instância de Orders é criada até que a Value propriedade da variável Lazy seja acessada pela primeira vez. No primeiro acesso, o tipo encapsulado é criado, retornado e armazenado para qualquer acesso subsequente.

// We need to create the array only if displayOrders is true
if (s_displayOrders == true)
{
    DisplayOrders(_orders.Value.OrderData);
}
else
{
    // Don't waste resources getting order data.
}
' We need to create the array only if _displayOrders is true
If _displayOrders = True Then
    DisplayOrders(_orders.Value.OrderData)
Else
    ' Don't waste resources getting order data.
End If

Um Lazy<T> objeto sempre retorna o mesmo objeto ou valor com o qual foi inicializado. Portanto, a propriedade Value é de leitura única. Se Value armazenar um tipo de referência, não será possível atribuir um novo objeto a ele. (No entanto, você pode alterar o valor de seus campos públicos configuráveis e propriedades.) Se Value armazenar um tipo de valor, não será possível modificar seu valor. No entanto, você pode criar uma nova variável invocando o construtor da 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 preguiçosa, como a anterior, não instancia Orders até que sua Value propriedade seja acessada pela primeira vez.

Inicialização segura de threads

Por padrão, Lazy<T> objetos são thread-safe. Ou seja, se o construtor não especificar o tipo de segurança de thread, os Lazy<T> objetos que ele cria são thread-safe. Em cenários multi-threaded, a primeira thread a aceder à propriedade Value de um objeto thread-safe Lazy<T> inicializa-o para todos os acessos subsequentes em todos os threads, e todos os threads partilham os mesmos dados. Portanto, não importa qual thread inicializa o objeto, e as condições de corrida são benignas.

Nota

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

O exemplo a seguir mostra que a mesma Lazy<int> instância 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(() => Environment.CurrentManagedThreadId);

Thread t1 = new(() => Console.WriteLine($"number on t1 = {number.Value} ThreadID = {Environment.CurrentManagedThreadId}"));
t1.Start();

Thread t2 = new(() => Console.WriteLine($"number on t2 = {number.Value} ThreadID = {Environment.CurrentManagedThreadId}"));
t2.Start();

Thread t3 = new(() => Console.WriteLine($"number on t3 = {number.Value} ThreadID = {Environment.CurrentManagedThreadId}"));
t3.Start();

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

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

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

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

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

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

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

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

Alguns Lazy<T> construtores têm um parâmetro Boolean chamado isThreadSafe que é usado para especificar se a Value propriedade será acessada a partir de várias threads. Para aceder à propriedade de apenas uma linha de execução, passe false para obter um benefício de desempenho modesto. Se pretender aceder à propriedade de múltiplas threads, passe true para instruir a instância Lazy<T> a lidar corretamente com condições de corrida nas quais uma thread lança uma exceção no momento da inicialização.

Alguns Lazy<T> construtores têm um LazyThreadSafetyMode parâmetro chamado mode. Esses construtores fornecem um modo de segurança de rosca adicional. A tabela a seguir mostra como a segurança de thread de um Lazy<T> objeto é afetada por parâmetros de construtor que especificam a segurança de thread. Cada construtor tem no máximo um desses parâmetros.

Segurança da rosca do objeto LazyThreadSafetyMode mode parâmetro Parâmetro booleano isThreadSafe Sem parâmetros de segurança de rosca
Totalmente seguro para threads; apenas um thread de cada vez tenta inicializar o valor. ExecutionAndPublication true Sim.
Não é thread-safe. None false Não aplicável.
Totalmente seguro para threads; as threads competem para inicializar o valor. PublicationOnly Não aplicável. Não aplicável.

Como mostra a tabela, especificar LazyThreadSafetyMode.ExecutionAndPublication para o mode parâmetro é o mesmo que especificar true para o isThreadSafe parâmetro, e especificar LazyThreadSafetyMode.None é o mesmo que especificar false.

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

A especificação LazyThreadSafetyMode.PublicationOnly permite que vários threads tentem inicializar a Lazy<T> instância. Apenas um thread pode vencer esta 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. As exceções não são armazenadas em cache, portanto, uma tentativa subsequente de acessar a Value propriedade pode resultar em inicialização bem-sucedida. Isso difere da maneira como as exceções são tratadas em outros modos, que é descrita na seção a seguir. Para obter mais informações, consulte a LazyThreadSafetyMode enumeração.

Exceções em objetos preguiçosos

Como dito anteriormente, um Lazy<T> objeto sempre retorna o mesmo objeto ou valor com o qual foi inicializado e, portanto, a Value propriedade é 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 do seu método de inicialização quando a Value propriedade for acessada pela primeira vez, essa mesma exceção será lançada em todas as tentativas subsequentes de acessar a propriedade Value. Em outras palavras, o construtor do tipo encapsulado nunca é invocado novamente, mesmo em cenários multithreaded. Portanto, o Lazy<T> objeto não pode lançar uma exceção em um acesso e retornar um valor em um acesso subsequente.

O cache de exceção é habilitado quando você usa qualquer System.Lazy<T> construtor que usa um método de inicialização (valueFactory parâmetro), por exemplo, ele é habilitado quando você usa o Lazy(T)(Func(T))construtor. Se o construtor também usa um LazyThreadSafetyMode valor (mode parâmetro), especifique LazyThreadSafetyMode.ExecutionAndPublication ou LazyThreadSafetyMode.None. A especificação de um método de inicialização habilita o cache de exceção 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()) no Visual Basic. Se você usar um System.Lazy<T> construtor que não especifica um método de inicialização, as exceções lançadas pelo construtor sem parâmetros para T não são armazenadas em cache. Para obter mais informações, consulte a LazyThreadSafetyMode enumeração.

Nota

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

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

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

Construtor Modo de segurança da rosca Usa o método de inicialização As exceções são armazenadas em cache
Preguiçoso(T)() (ExecutionAndPublication) Não Não
Preguiçoso(T)(Func(T)) (ExecutionAndPublication) Sim Sim
Preguiçoso (T)(Booleano) True (ExecutionAndPublication) ou false (None) Não Não
Preguiçoso(T)(Func(T), Booleano) True (ExecutionAndPublication) ou false (None) Sim Sim
Preguiçoso (T)(LazyThreadSafetyMode) Especificado pelo usuário Não Não
Preguiçoso (T)(Func(T), LazyThreadSafetyMode) Especificado pelo usuário Sim Não se o usuário especificar PublicationOnly, caso contrário, sim.

Implementando uma propriedade com inicialização tardia

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

class Customer
{
    private readonly Lazy<Orders> _orders;
    public string CustomerID { get; private set; }
    public Customer(string id)
    {
        CustomerID = id;
        _orders = new Lazy<Orders>(() =>
        {
            // You can specify any additional
            // initialization steps here.
            return new Orders(CustomerID);
        });
    }

    public Orders MyOrders
    {
        get
        {
            // Orders is created on first access here.
            return _orders.Value;
        }
    }
}
Class Customer
    Private _orders As Lazy(Of Orders)
    Public Shared CustomerID As String
    Public Sub New(ByVal id As String)
        CustomerID = id
        _orders = New Lazy(Of Orders)(Function()
                                          ' You can specify additional 
                                          ' initialization steps here
                                          Return New Orders(CustomerID)
                                      End Function)

    End Sub
    Public ReadOnly Property MyOrders As Orders

        Get
            Return _orders.Value
        End Get

    End Property

End Class

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

Inicialização preguiçosa de thread local

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

[ThreadStatic]
static int s_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 System.Threading.ThreadLocal<T> tipo para criar uma variável local de thread baseada em instância que é inicializada em todos os threads pelo Action<T> delegado fornecido. No exemplo a seguir, todos os threads acessados counter verão seu valor inicial como 1.

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

ThreadLocal<T> envolve o seu objeto da mesma forma que Lazy<T>, com estas diferenças essenciais:

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

  • A ThreadLocal<T>.Value propriedade é de leitura e gravação, podendo ser modificada quantas vezes forem necessárias. Isso pode afetar a propagação de exceções, por exemplo, uma get operação pode gerar uma exceção, mas a próxima pode inicializar com êxito o valor.

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

O exemplo a seguir demonstra que cada thread que acessa a ThreadLocal<int> instância 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(() => Environment.CurrentManagedThreadId);
Thread t4 = new(() => Console.WriteLine($"threadLocalNumber on t4 = {threadLocalNumber.Value} ThreadID = {Environment.CurrentManagedThreadId}"));
t4.Start();

Thread t5 = new(() => Console.WriteLine($"threadLocalNumber on t5 = {threadLocalNumber.Value} ThreadID = {Environment.CurrentManagedThreadId}"));
t5.Start();

Thread t6 = new(() => Console.WriteLine($"threadLocalNumber on t6 = {threadLocalNumber.Value} ThreadID = {Environment.CurrentManagedThreadId}"));
t6.Start();

// Ensure that thread IDs are not recycled if the
// first thread completes before the last one starts.
t4.Join();
t5.Join();
t6.Join();

/* Sample Output:
   threadLocalNumber on t4 = 14 ThreadID = 14
   threadLocalNumber on t5 = 15 ThreadID = 15
   threadLocalNumber on t6 = 16 ThreadID = 16
*/
' Initialize the integer to the managed thread id on a per-thread basis.
Dim threadLocalNumber As New ThreadLocal(Of Integer)(Function() Thread.CurrentThread.ManagedThreadId)
Dim t4 As New Thread(Sub()
                         Console.WriteLine("number on t4 = {0} threadID = {1}",
                                           threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
t4.Start()

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

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

' Ensure that thread IDs are not recycled if the 
' first thread completes before the last one starts.
t4.Join()
t5.Join()
t6.Join()

'Sample(Output)
'      threadLocalNumber on t4 = 14 ThreadID = 14 
'      threadLocalNumber on t5 = 15 ThreadID = 15
'      threadLocalNumber on t6 = 16 ThreadID = 16 

Variáveis Thread-Local em Parallel.For e ForEach

Quando utiliza os métodos Parallel.For ou Parallel.ForEach para iterar sobre fontes de dados em paralelo, pode usar as sobrecargas que têm suporte interno para dados locais de thread. Nesses métodos, a localidade de thread é obtida usando delegados locais para criar, acessar e limpar os dados. Para obter mais informações, consulte Como: Escrever um loop Parallel.For com variáveis locais de thread e Como: Escrever 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 inicializar com preguiça um grande número de objetos, você pode decidir que envolver cada objeto em um Lazy<T> requer muita memória ou muitos recursos de computação. Ou, você pode ter requisitos rigorosos sobre como a inicialização lenta é exposta. Nesses casos, pode usar os métodos static da classe Shared (System.Threading.LazyInitializer no Visual Basic) para inicializar de forma preguiçosa cada objeto sem envolvê-lo numa instância de Lazy<T>.

No exemplo a seguir, suponha que, em vez de envolver um objeto inteiro Orders em um Lazy<T> objeto, você tenha objetos individuais Order inicializados com lentidão somente se eles forem 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 multi-threaded, o primeiro thread a invocar o procedimento de inicialização é aquele cujo valor é visto por todos os threads. Threads posteriores também invocam o procedimento de inicialização, mas seus resultados não são usados. Se este tipo de condição de corrida potencial não for aceitável, use a versão sobrecarregada de LazyInitializer.EnsureInitialized que aceita um argumento booleano e um objeto de sincronização.

Consulte também