Inicialización diferida
La inicialización diferida de un objeto implica que su creación se aplaza hasta que se usa por primera vez. (En este tema, los términos inicialización diferida y creación diferida de instancias son sinónimos). La inicialización diferida se usa principalmente para mejorar el rendimiento, evitar la pérdida de tiempo en los cálculosy reducir los requisitos de memoria de los programas. Estos son los escenarios más comunes:
Cuando hay un objeto costoso de crear y es posible que el programa no lo use. Por ejemplo, supongamos que tiene en memoria un objeto
Customer
con una propiedadOrders
que contiene una matriz grande de objetosOrder
que, para inicializarse, requieren una conexión de base de datos. Si el usuario nunca solicita que se muestre Orders y nunca usa los datos en un cálculo, no hay ninguna razón para usar la memoria del sistema o ciclos de cálculos para crearlo. Mediante el uso deLazy<Orders>
para declarar el objetoOrders
para la inicialización diferida, puede evitar desperdiciar recursos del sistema si no se usa el objeto.Cuando hay un objeto costoso de crear y quiere diferir su creación hasta después de que se hayan completado otras operaciones costosas. Por ejemplo, supongamos que el programa carga varias instancias de objeto cuando se inicia, pero solo se necesitan de inmediato algunas de ellas. Puede mejorar el rendimiento de inicio del programa si difiere la inicialización de los objetos que no son necesarios hasta que se hayan creado los objetos necesarios.
Aunque puede escribir su propio código para llevar a cabo la inicialización diferida, recomendamos que use Lazy<T> en su lugar. Lazy<T> y sus tipos relacionados también admiten la seguridad para subprocesos y ofrecen una directiva coherente de propagación de excepciones.
En la tabla siguiente se muestran los tipos que .NET Framework versión 4 proporciona para habilitar la inicialización diferida en distintos escenarios.
Tipo | Descripción |
---|---|
Lazy<T> | Clase contenedora que proporciona semántica de inicialización diferida para cualquier biblioteca de clases o tipo definido por el usuario. |
ThreadLocal<T> | Se parece a Lazy<T>, con la diferencia de que proporciona semántica de inicialización diferida para cada subproceso local. Cada subproceso tiene acceso a su propio valor único. |
LazyInitializer | Proporciona métodos static avanzados (Shared en Visual Basic) para la inicialización diferida de objetos sin la sobrecarga de una clase. |
Inicialización diferida básica
Para definir un tipo con inicialización diferida, como MyType
, use Lazy<MyType>
(Lazy(Of MyType)
en Visual Basic), como se muestra en el ejemplo siguiente. Si no se pasa ningún delegado en el constructor Lazy<T>, el tipo contenedor se crea mediante Activator.CreateInstance cuando se tiene acceso por primera vez a la propiedad de valor. Si el tipo no tiene un constructor sin parámetros, se genera una excepción en tiempo de ejecución.
En el ejemplo siguiente, supongamos que Orders
es una clase que contiene una matriz de objetos Order
recuperados de una base de datos. Un objeto Customer
contiene una instancia de Orders
, pero en función de las acciones del usuario, los datos del objeto Orders
podrían no ser necesarios.
// 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)()
También se puede pasar un delegado en el constructor Lazy<T> que invoca una sobrecarga de constructor específica en el tipo ajustado en tiempo de creación y realizar los pasos de inicialización que se requieran, como se muestra en el ejemplo siguiente.
// 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))
Una vez que se ha creado el objeto diferido, no se crea ninguna instancia de Orders
mientras no se tenga acceso por primera vez a la propiedad Value de la variable Lazy. En el primer acceso, el tipo encapsulado se crea y se devuelve, y se almacena para cualquier acceso 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
Un objeto Lazy<T> siempre devuelve el mismo objeto o valor con el que se ha inicializado. Por lo tanto, la propiedad Value es de solo lectura. Si Value almacena un tipo de referencia, no se le puede asignar un nuevo objeto. (Pero puede cambiar el valor de sus propiedades y campos públicos configurables). Si Value almacena un tipo de valor, no puede modificar su valor. Aun así, puede crear una variable si invoca de nuevo el constructor de la variable con argumentos nuevos.
_orders = new Lazy<Orders>(() => new Orders(10));
_orders = New Lazy(Of Orders)(Function() New Orders(10))
La nueva instancia diferida, al igual que la anterior, no crea instancias de Orders
mientras no se tenga acceso por primera vez a su propiedad Value.
Inicialización segura para subprocesos
De forma predeterminada, los objetos Lazy<T> son seguros para subprocesos. Es decir, si el constructor no especifica el tipo de seguridad para subprocesos, los objetos Lazy<T> que crea son seguros para subprocesos. En escenarios multiproceso, el primer subproceso que tiene acceso a la propiedad Value de un objeto Lazy<T> seguro para subprocesos lo inicializa para todos los accesos siguientes en todos los subprocesos, y todos los subprocesos comparten los mismos datos. Por lo tanto, no importa qué subproceso inicializa el objeto y las condiciones de carrera son benignas.
Nota
Puede ampliar esta coherencia a las condiciones de error mediante el uso del almacenamiento en caché de excepciones. Para obtener más información, vea la próxima sección titulada Excepciones en objetos diferidos.
En el ejemplo siguiente se muestra que la instancia Lazy<int>
tiene el mismo valor para tres subprocesos independientes.
// 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.
Si necesita datos independientes en cada subproceso, use el tipo ThreadLocal<T>, como se describe más adelante en este tema.
Algunos constructores Lazy<T> tienen un parámetro booleano denominado isThreadSafe
que se usa para especificar si se obtendrá acceso a la propiedad Value desde varios subprocesos. Si piensa tener acceso a la propiedad desde un solo subproceso, pase false
para obtener una pequeña mejora en el rendimiento. Si piensa tener acceso a la propiedad desde varios subprocesos, pase true
para indicarle a la instancia Lazy<T> que controle correctamente las condiciones de carrera en las que un subproceso produce una excepción durante la inicialización.
Algunos constructores Lazy<T> tienen un parámetro LazyThreadSafetyMode denominado mode
. Estos constructores proporcionan un modo adicional de seguridad para subprocesos. En la tabla siguiente se muestra la manera en que la seguridad para subprocesos de un objeto Lazy<T> se ve afectada por los parámetros del constructor que especifican la seguridad para subprocesos. Cada constructor tiene como máximo un parámetro de este tipo.
Seguridad para subprocesos del objeto | Parámetro LazyThreadSafetyMode mode |
Parámetro booleano isThreadSafe |
Sin parámetros de seguridad para subprocesos |
---|---|---|---|
Totalmente seguro para subprocesos; solo intenta inicializar el valor un subproceso de cada vez. | ExecutionAndPublication | true |
Sí. |
No es seguro para subprocesos. | None | false |
No aplicable. |
Totalmente seguro para subprocesos; los subprocesos se apresuran a inicializar el valor. | PublicationOnly | No es aplicable. | No es aplicable. |
Como se muestra en la tabla, especificar LazyThreadSafetyMode.ExecutionAndPublication para el parámetro mode
equivale a especificar true
para el parámetro isThreadSafe
, y especificar LazyThreadSafetyMode.None equivale a especificar false
.
Para más información sobre a qué hacen referencia Execution
y Publication
, consulte LazyThreadSafetyMode.
Si se especifica LazyThreadSafetyMode.PublicationOnly, varios subprocesos pueden intentar inicializar la instancia Lazy<T>. Solo un subproceso puede ganar esta carrera, y los demás subprocesos recibirán el valor que haya inicializado el subproceso ganador. Si se produce una excepción en un subproceso durante la inicialización, dicho subproceso no recibe el valor establecido por el subproceso ganador. Las excepciones no se almacenan en caché, por lo que un intento posterior para tener acceso a la propiedad Value puede dar lugar a un inicialización correcta. Esto difiere de la manera en que se tratan las excepciones en otros modos, como se describe en la sección siguiente. Para obtener más información, vea la enumeración LazyThreadSafetyMode.
Excepciones en objetos diferidos
Como ya se ha indicado, un objeto Lazy<T> siempre devuelve el mismo objeto o valor con el que se ha inicializado y, por tanto, la propiedad Value es de solo lectura. Si habilita el almacenamiento en caché de excepciones, esta inmutabilidad también se aplica al comportamiento de las excepciones. Si un objeto con inicialización diferida tiene habilitado el almacenamiento en caché de excepciones y genera una excepción desde su método de inicialización cuando se obtiene acceso por primera vez a la propiedad Value, se produce esa misma excepción en cada intento posterior de acceder a la propiedad Value. En otras palabras, el constructor del tipo encapsulado nunca se vuelve a invocar, ni siquiera en escenarios multiproceso. Por lo tanto, el objeto Lazy<T> no puede producir una excepción en un acceso y devolver un valor en un acceso posterior.
El almacenamiento en caché de excepciones se habilita cuando se usa cualquier constructor System.Lazy<T> que toma un método de inicialización (un parámetro valueFactory
); por ejemplo, se habilita cuando se usa el constructor Lazy(T)(Func(T))
. Si el constructor también toma un valor LazyThreadSafetyMode (un parámetro mode
), especifique LazyThreadSafetyMode.ExecutionAndPublication o LazyThreadSafetyMode.None. Al especificar un método de inicialización, se permite el almacenamiento en caché de excepciones para estos dos modos. El método de inicialización puede ser muy simple. Por ejemplo, podría llamar al constructor sin parámetros para T
: new Lazy<Contents>(() => new Contents(), mode)
en C# o New Lazy(Of Contents)(Function() New Contents())
en Visual Basic. Si usa un constructor System.Lazy<T> que no especifica un método de inicialización, las excepciones que inicie el constructor sin parámetros para T
no se almacenarán en caché. Para obtener más información, vea la enumeración LazyThreadSafetyMode.
Nota
Si crea un objeto Lazy<T> con el parámetro de constructor isThreadSafe
establecido en false
o el parámetro de constructor mode
establecido en LazyThreadSafetyMode.None, debe tener acceso al objeto Lazy<T> desde un subproceso o proporcionar su propia sincronización. Esto se aplica a todos los aspectos del objeto, incluido el almacenamiento en caché de excepciones.
Como ya se ha indicado en la sección anterior, los objetos Lazy<T> creados al especificar LazyThreadSafetyMode.PublicationOnly tratan las excepciones de forma diferente. Con PublicationOnly, varios subprocesos pueden competir para inicializar la instancia Lazy<T>. En este caso, las excepciones no se almacenan en caché y los intentos para obtener acceso a la propiedad Value pueden continuar hasta que se complete la inicialización.
En la tabla siguiente se resume la forma en que los constructores Lazy<T> controlan el almacenamiento en caché de excepciones.
Constructor | Modo de seguridad para subprocesos | Usa método de inicialización | Las excepciones se almacenan en caché |
---|---|---|---|
Lazy(T)() | (ExecutionAndPublication) | No | No |
Lazy(T)(Func(T)) | (ExecutionAndPublication) | Sí | Sí |
Lazy(T)(Boolean) | True (ExecutionAndPublication) o false (None) |
No | No |
Lazy(T)(Func(T), Boolean) | True (ExecutionAndPublication) o false (None) |
Sí | Sí |
Lazy(T)(LazyThreadSafetyMode) | Especificado por el usuario | No | No |
Lazy(T)(Func(T), LazyThreadSafetyMode) | Especificado por el usuario | Yes | No si el usuario especifica PublicationOnly; en caso contrario, sí. |
Implementar una propiedad con inicialización diferida
Para implementar una propiedad pública mediante la inicialización diferida, defina el campo de respaldo de la propiedad como Lazy<T> y devuelva la propiedad Value desde el descriptor de acceso get
de la propiedad.
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
La propiedad Value es de solo lectura; por lo tanto, la propiedad que la expone no tiene ningún descriptor de acceso set
. Si necesita una propiedad de lectura/escritura respaldada por un objeto Lazy<T>, el descriptor de acceso set
debe crear un objeto Lazy<T> y asignarlo a la memoria auxiliar. El descriptor de acceso set
debe crear una expresión lambda que devuelva el nuevo valor de propiedad que se ha pasado al descriptor de acceso set
y pasar dicha expresión lambda al constructor para el nuevo objeto Lazy<T>. El siguiente acceso de la propiedad Value provocará la inicialización del nuevo objeto Lazy<T>, y su propiedad Value devolverá a partir de entonces el nuevo valor que se ha asignado a la propiedad. El objetivo de este complicado proceso consiste en conservar las protecciones multiproceso integradas en Lazy<T>. De lo contrario, los descriptores de acceso de propiedad tendrían que almacenar en caché el primer valor devuelto por la propiedad Value y modificar solo el valor almacenado en caché, y usted tendría que escribir su propio código seguro para subprocesos para hacerlo. Debido a las inicializaciones adicionales que requiere una propiedad de lectura/escritura respaldada por un objeto Lazy<T>, el rendimiento podría no ser aceptable. Además, en función del escenario, podría ser necesaria una coordinación adicional para evitar condiciones de carrera entre los establecedores y los captadores.
Inicialización diferida local de subprocesos
En algunos escenarios multiproceso, podría interesarle asignarle a cada subproceso sus propios datos privados. Estos datos se denominan datos locales de subproceso. En .NET Framework versión 3.5 y anteriores, se podía aplicar el atributo ThreadStatic
a una variable estática para convertirla en una variable local de subproceso. Pero el uso del atributo ThreadStatic
puede producir pequeños errores. Por ejemplo, incluso las instrucciones de inicialización básicas harán que la variable solo se inicialice en el primer subproceso que tenga acceso a ella, tal como se muestra en el ejemplo siguiente.
[ThreadStatic]
static int counter = 1;
<ThreadStatic()>
Shared counter As Integer
En todos los demás subprocesos, la variable se inicializará mediante su valor predeterminado (cero). Como alternativa en .NET Framework versión 4, puede usar el tipo System.Threading.ThreadLocal<T> para crear una variable local de subproceso basada en instancias que se inicialice en todos los subprocesos mediante el delegado Action<T> que proporcione. En el ejemplo siguiente, todos los subprocesos que tienen acceso a counter
tendrán 1 como valor inicial.
ThreadLocal<int> betterCounter = new ThreadLocal<int>(() => 1);
Dim betterCounter As ThreadLocal(Of Integer) = New ThreadLocal(Of Integer)(Function() 1)
ThreadLocal<T> encapsula el objeto de la misma manera que Lazy<T>, pero con estas diferencias básicas:
Cada subproceso inicializa la variable local de subproceso mediante sus propios datos privados, que no son accesibles desde otros subprocesos.
La propiedad ThreadLocal<T>.Value es de lectura y escritura y se puede modificar todas las veces que se quiera. Esto puede afectar a la propagación de excepciones; por ejemplo, una operación
get
puede producir una excepción, pero la siguiente puede inicializar correctamente el valor.Si no se proporciona ningún delegado de inicialización, ThreadLocal<T> inicializará su tipo encapsulado mediante el valor predeterminado del tipo. En este sentido, ThreadLocal<T> es coherente con el atributo ThreadStaticAttribute.
En el ejemplo siguiente se muestra que cada subproceso que tiene acceso a la instancia ThreadLocal<int>
obtiene su propia copia única de los datos.
// 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
Variables locales de subproceso en Parallel.For y ForEach
Cuando se usa el método Parallel.For o Parallel.ForEach para iterar en los orígenes de datos en paralelo, puede usar las sobrecargas que tienen compatibilidad integrada con datos locales de subproceso. En estos métodos, la localidad del subproceso se logra mediante el uso de delegados locales para crear los datos, obtener acceso a ellos y limpiarlos. Para obtener más información, vea Cómo: Escribir un bucle Parallel.For con variables locales de subproceso y Cómo: Escribir un bucle Parallel.ForEach con variables locales de partición.
Usar la inicialización diferida para escenarios con poca sobrecarga
En los escenarios en los que tiene que inicializar de forma diferida un gran número de objetos, podría decidir que el proceso de encapsular cada objeto en un objeto Lazy<T> requiere demasiada memoria o demasiados recursos informáticos. O bien, es posible que tenga requisitos estrictos sobre cómo se expone la inicialización diferida. En tales casos, puede usar los métodos static
(Shared
en Visual Basic) de la clase System.Threading.LazyInitializer para inicializar de forma diferida cada objeto sin encapsularlo en una instancia de Lazy<T>.
En el ejemplo siguiente se da por supuesto que, en lugar de encapsular todo un objeto Orders
en un objeto Lazy<T>, solo se inicializan de forma diferida los objetos Order
individuales si son necesarios.
// 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
En este ejemplo, observe que el procedimiento de inicialización se invoca en cada iteración del bucle. En escenarios multiproceso, el primer subproceso que invoca el procedimiento de inicialización es aquel cuyo valor ven todos los subprocesos. Los subprocesos posteriores también invocan el procedimiento de inicialización, pero sus resultados no se usan. Si este tipo de condición de carrera potencial no es aceptable, use la sobrecarga de LazyInitializer.EnsureInitialized que toma un argumento booleano y un objeto de sincronización.