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 umaOrders
propriedade que contenha uma grande matriz de objetos que, para ser inicializado, requer uma conexão de banco deOrder
dados. Se o usuário nunca pedir para exibir os pedidos ou usar os dados em um cálculo, então não há razão para usar a memória do sistema ou ciclos de computação para criá-lo.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 | Description |
---|---|
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 Lazy<T> , exceto que ele fornece semântica de inicialização lenta em uma base de thread-local. 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, MyType
use 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 encapsulado será criado usando Activator.CreateInstance quando a propriedade value for 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 recuperados de um banco de Order
dados. Um Customer
objeto contém uma instância de , mas dependendo das ações do Orders
usuário, os dados do Orders
objeto 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 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 Lazy<T> construtor que invoca uma sobrecarga de construtor específica no tipo encapsulado no momento da 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 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 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 e retornado e armazenado para qualquer acesso futuro.
// 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 Lazy<T> objeto sempre retorna o mesmo objeto ou valor com o qual foi inicializado. Portanto, a Value propriedade é somente leitura. 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 para threads
Por padrão, Lazy<T> os 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, o primeiro thread a acessar a Value propriedade de um objeto thread-safe Lazy<T> o inicializa 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.
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 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 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 propriedade será acessada Value a partir de vários threads. Se você pretende acessar a propriedade a partir de apenas um fio, passe false
para obter um benefício de desempenho modesto. Se você pretende acessar a propriedade de vários threads, passe true
para instruir a Lazy<T> instância a lidar corretamente com condições de corrida nas quais um 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 roscas; 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 roscas; threads correm 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
consulte, 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 de seu método de inicialização quando a propriedade for acessada Value pela primeira vez, essa mesma exceção será lançada em todas as tentativas subsequentes de acessar a Value propriedade. 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 PublicationOnlyo , vários threads podem competir para inicializar a Lazy<T> instância. Nesse caso, as exceções não são armazenadas em cache e as tentativas de acessar a propriedade podem continuar até que a Value 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) | No | Não |
Preguiçoso(T)(Func(T)) | (ExecutionAndPublication) | Sim | Sim |
Preguiçoso (T)(Booleano) | True (ExecutionAndPublication) ou false (None) |
No | Não |
Preguiçoso(T)(Func(T), Booleano) | True (ExecutionAndPublication) ou false (None) |
Sim | Sim |
Preguiçoso (T)(LazyThreadSafetyMode) | Especificado pelo usuário | No | 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 inicializada com preguiça
Para implementar uma propriedade pública usando inicialização lenta, defina o campo de suporte da propriedade como um Lazy<T>e devolva a Value get
propriedade do acessador 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 Value propriedade é somente leitura, portanto, a propriedade que a expõe não set
tem acessório. 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 multithreaded, você pode querer dar a cada thread seus próprios dados privados. Esses dados são chamados de dados thread-local. 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 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 ThreadLocal<int>(() => 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 é leitura-gravação e pode ser modificada qualquer número de vezes. 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 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 Thread-Local em Parallel.For e ForEach
Quando você usa o Parallel.For método ou Parallel.ForEach método para iterar sobre fontes de dados em paralelo, você pode usar as sobrecargas que têm suporte interno para dados thread-local. 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 Thread-Local e Como escrever um loop Parallel.ForEach com variáveis Partition-Local.
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, você pode usar os static
métodos (Shared
no Visual Basic) da System.Threading.LazyInitializer classe para inicializar com preguiça cada objeto sem envolvê-lo em uma 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 esse tipo de condição de corrida potencial não for aceitável, use a sobrecarga de que leva um argumento booleano e um objeto de LazyInitializer.EnsureInitialized sincronização.