Initialisation tardive

L’initialisation tardive d’un objet signifie que sa création est différée jusqu’à sa première utilisation. (Pour cette rubrique, les termes initialisation tardive et instanciation tardive sont synonymes.) L’initialisation tardive est principalement utilisée pour améliorer les performances, éviter les calculs inutiles et réduire les besoins en mémoire programme. Voici les scénarios les plus courants :

  • Lorsqu’un objet est coûteux à créer, et qu’il est possible que le programme ne l’utilise pas. Par exemple, supposons que vous ayez en mémoire un objet Customer avec une propriété Orders contenant un grand tableau d’objets Order qui, pour être initialisé, nécessite une connexion de base de données. Si l’utilisateur ne demande jamais à afficher les commandes ou à utiliser les données dans un calcul, il est inutile d’utiliser la mémoire système ou les cycles de calcul pour les créer. En utilisant Lazy<Orders> pour déclarer l’objet Orders en vue de son initialisation tardive, vous évitez de gaspiller les ressources système lorsque l’objet n’est pas utilisé.

  • Si vous avez un objet qui est coûteux à créer, et si vous souhaitez différer sa création jusqu’à ce que d’autres opérations coûteuses soient terminées. Par exemple, supposons que votre programme charge plusieurs instances d’objet lorsqu’il démarre, mais que seules certaines d’entre elles soient immédiatement nécessaires. Vous pouvez améliorer les performances de démarrage du programme en différant l’initialisation des objets qui ne sont pas nécessaires tant que les objets nécessaires n’ont pas été créés.

Même si vous pouvez écrire votre propre code pour effectuer une initialisation tardive, nous vous recommandons d’utiliser Lazy<T>. Lazy<T> et ses types associés prennent également en charge la cohérence de thread et fournissent une stratégie cohérente de propagation des exceptions.

Le tableau suivant répertorie les types fournis par le .NET Framework version 4 pour permettre l’initialisation tardive dans différents scénarios.

Type Description
Lazy<T> Classe wrapper qui fournit une sémantique d’initialisation tardive pour toute bibliothèque de classes ou type défini par l’utilisateur.
ThreadLocal<T> Similaire à Lazy<T>, sauf qu’il fournit une sémantique d’initialisation tardive en fonction du thread local. Chaque thread a accès à sa propre valeur.
LazyInitializer Fournit des méthodes static avancées (Shared en Visual Basic) pour l’initialisation tardive des objets, sans la surcharge d’une classe.

Initialisation tardive de base

Pour définir un type initialisé tardivement (par exemple, MyType), utilisez Lazy<MyType> (Lazy(Of MyType) en Visual Basic), comme illustré dans l’exemple suivant. Si aucun délégué n’est passé dans le constructeur Lazy<T>, le type encapsulé est créé à l’aide de Activator.CreateInstance lors du premier accès à la propriété de la valeur. Si le type n’a pas de constructeur sans paramètre, une exception runtime est levée.

Dans l’exemple suivant, supposons que Orders soit une classe qui contienne un tableau d’objets Order récupérés à partir d’une base de données. Un objet Customer contient une instance de Orders, mais en fonction des actions de l’utilisateur, les données de l’objet Orders peuvent ne pas être nécessaires.

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

Vous pouvez également passer un délégué dans le constructeur Lazy<T> qui appelle une surcharge de constructeur sur le type encapsulé au moment de la création, et effectuer d’autres étapes d’initialisation nécessaires, comme indiqué dans l’exemple suivant.

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

Une fois que l’objet différé est créé, aucune instance de Orders n’est créée tant que la propriété Value de la variable tardive n’a pas fait l’objet d’un accès. Lors du premier accès à la propriété, le type encapsulé est créé, retourné, puis stocké en vue d’une utilisation ultérieure.

// 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 objet Lazy<T> retourne toujours le même objet (ou la même valeur) qui a été utilisé lors de son initialisation. Par conséquent, la propriété Value est en lecture seule. Si Value stocke un type référence, vous ne pouvez pas lui attribuer un nouvel objet (Toutefois, vous pouvez modifier la valeur de ses champs et de ses propriétés publics paramétrables.) Si Value stocke un type de valeur, vous ne pouvez pas modifier sa valeur. Toutefois, vous pouvez créer une variable en rappelant le constructeur de variable à l’aide de nouveaux arguments.

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

La nouvelle instance tardive, comme la précédente, n’instancie pas Orders tant que sa propriété Value n’a pas fait l’objet d’un accès.

Initialisation thread-safe

Par défaut, les objets Lazy<T> sont thread-safe. Autrement dit, si le constructeur ne spécifie pas le type de cohérence de thread, les objets Lazy<T> qu’il crée sont thread-safe. Dans les scénarios multithreads, le premier thread qui accède à la propriété Value d’un objet Lazy<T> thread-safe initialise celui-ci pour tous les accès suivants sur tous les threads. De plus, tous les threads partagent les mêmes données. Par conséquent, le thread qui initialise l’objet importe peu, et les conditions de concurrence sont sans conséquences.

Notes

Vous pouvez étendre cette cohérence aux conditions d’erreur à l’aide de la mise en cache des exceptions. Pour plus d’informations, consultez la section suivante, Exceptions des objets différés.

L’exemple suivant montre que la même instance Lazy<int> a la même valeur pour trois threads différents.

// 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 chaque thread doit contenir des données distinctes, utilisez le type ThreadLocal<T>, comme décrit plus loin dans cette rubrique.

Certains constructeurs Lazy<T> ont un paramètre booléen nommé isThreadSafe qui permet de spécifier si la propriété Value est accessible à partir de plusieurs threads. Si vous envisagez d’accéder à la propriété à partir d’un seul thread, vous pouvez passer false pour obtenir un gain de performances modeste. Si vous avez l’intention d’accéder à la propriété à partir de plusieurs threads, passez true pour indiquer à l’instance Lazy<T> qu’elle doit gérer correctement les conditions de concurrence dans lesquelles un thread lève une exception au moment de l’initialisation.

Certains constructeurs Lazy<T> ont un paramètre LazyThreadSafetyMode nommé mode. Ces constructeurs fournissent un mode de cohérence de thread supplémentaire. Le tableau suivant montre comment la cohérence de thread d’un objet Lazy<T> est affectée par les paramètres du constructeur qui spécifient la cohérence de thread. Chaque constructeur comprend un tel paramètre.

Cohérence de thread de l’objet Paramètre LazyThreadSafetyModemode Paramètre isThreadSafe booléen Aucun paramètre de cohérence de thread
Entièrement thread-safe. Seul un thread à la fois tente d’initialiser la valeur. ExecutionAndPublication true Oui.
Non thread-safe. None false Non applicable.
Entièrement thread-safe. Concurrence de threads pour initialiser la valeur. PublicationOnly Non applicable. Non applicable.

Comme le montre le tableau, spécifier LazyThreadSafetyMode.ExecutionAndPublication pour le paramètre mode équivaut à spécifier true pour le paramètre isThreadSafe, et spécifier LazyThreadSafetyMode.None revient à spécifier false.

Pour plus d’informations sur ce à quoi Execution et Publication font référence, consultez LazyThreadSafetyMode.

Le fait de spécifier LazyThreadSafetyMode.PublicationOnly permet à plusieurs threads de tenter d’initialiser l’instance Lazy<T>. Seul un thread peut gagner cette course. Tous les autres threads reçoivent la valeur qui a été initialisée par le thread gagnant. Si une exception est levée sur un thread pendant l’initialisation, ce thread ne reçoit pas la valeur définie par le thread gagnant. Les exceptions ne sont pas mises en cache. De fait, une nouvelle tentative d’accès à la propriété Value peut aboutir à une initialisation. Ce traitement des exceptions est différent de celui des autres modes, et fait l’objet de la section suivante. Pour plus d’informations, consultez l’énumération LazyThreadSafetyMode.

Exceptions des objets différés

Comme indiqué précédemment, un objet Lazy<T> retourne toujours le même objet (ou la même valeur) avec lequel il a été initialisé. De fait, la propriété Value est en lecture seule. Si vous activez la mise en cache des exceptions, cette immuabilité s’étend également au comportement des exceptions. Si la mise en cache des exceptions est activée pour un objet à initialisation tardive, et que celui-ci lève une exception à partir de sa méthode d’initialisation lors du premier accès à la propriété Value, cette même exception est levée à chaque tentative suivante d’accès à la propriété Value. En d’autres termes, le constructeur du type encapsulé n’est jamais rappelé, même dans les scénarios multithreads. Par conséquent, l’objet Lazy<T> ne peut pas lever une exception lors d’un accès et retourner une valeur lors d’un accès ultérieur.

La mise en cache des exceptions est activée lorsque vous utilisez un constructeur System.Lazy<T> qui accepte une méthode d’initialisation (un paramètre valueFactory). Par exemple, elle est activée lorsque vous utilisez le constructeur Lazy(T)(Func(T)). Si le constructeur accepte également une valeur LazyThreadSafetyMode (un paramètre mode), spécifiez LazyThreadSafetyMode.ExecutionAndPublication ou LazyThreadSafetyMode.None. La spécification d’une méthode d’initialisation permet la mise en cache des exceptions pour ces deux modes. La méthode d’initialisation peut être très simple. Par exemple, elle peut appeler le constructeur sans paramètre de T (new Lazy<Contents>(() => new Contents(), mode) en C# ou New Lazy(Of Contents)(Function() New Contents()) en Visual Basic). Si vous utilisez un constructeur System.Lazy<T> qui ne spécifie pas de méthode d’initialisation, les exceptions levées par le constructeur sans paramètre pour T ne sont pas mises en cache. Pour plus d’informations, consultez l’énumération LazyThreadSafetyMode.

Notes

Si vous créez un objet Lazy<T> avec le paramètre de constructeur isThreadSafe défini sur false ou le paramètre de constructeur mode défini sur LazyThreadSafetyMode.None, vous devez accéder à l’objet Lazy<T> à partir d’un thread unique ou fournir votre propre synchronisation. Cela s’applique à tous les aspects de l’objet, y compris la mise en cache des exceptions.

Comme indiqué dans la section précédente, les objets Lazy<T> créés en spécifiant LazyThreadSafetyMode.PublicationOnly traitent les exceptions différemment. Avec PublicationOnly, plusieurs threads peuvent rivaliser pour initialiser l’instance Lazy<T>. Dans ce cas, les exceptions ne sont pas mises en cache, et les tentatives d’accès à la propriété Value peuvent continuer jusqu’à ce que l’initialisation soit effectuée.

Le tableau suivant récapitule la façon dont les constructeurs Lazy<T> contrôlent la mise en cache des exceptions.

Constructeur Mode de cohérence de thread Utilise la méthode d’initialisation Exceptions mises en cache
Lazy(T)() (ExecutionAndPublication) Non Non
Lazy(T)(Func(T)) (ExecutionAndPublication) Oui Oui
Lazy(T)(Boolean) True (ExecutionAndPublication) ou false (None) Non Non
Lazy(T)(Func(T), Boolean) True (ExecutionAndPublication) ou false (None) Oui Oui
Lazy(T)(LazyThreadSafetyMode) Spécifié par l’utilisateur Non Non
Lazy(T)(Func(T), LazyThreadSafetyMode) Spécifié par l’utilisateur Oui Non, si l’utilisateur spécifie PublicationOnly ; sinon, Oui.

Implémentation d’une propriété à initialisation tardive

Pour implémenter une propriété publique à l’aide de l’initialisation tardive, définissez le champ de stockage de la propriété comme un Lazy<T>, puis retournez la propriété Value à partir de l’accesseur get de la propriété.

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 propriété Value est en lecture seule, par conséquent, la propriété qui l’expose n’a pas d’accesseur set. Si vous avez besoin d’une propriété en lecture/écriture stockée par un objet Lazy<T>, l’accesseur set doit créer un nouvel objet Lazy<T> et l’assigner au magasin de stockage. L’accesseur set doit créer une expression lambda qui retourne la nouvelle valeur de propriété qui a été passée à l’accesseur set, et passer cette expression lambda au constructeur pour le nouvel objet Lazy<T>. Le prochain accès à la propriété Value va entraîner l’initialisation du nouveau Lazy<T>, et sa propriété Value va alors retourner la nouvelle valeur qui a été affectée à la propriété. L’objectif de cet arrangement complexe est de conserver les protections de multithreading intégrées à Lazy<T>. Sans cela, les accesseurs de propriété devraient mettre en cache la première valeur retournée par la propriété Value et modifier uniquement la valeur mise en cache. En plus, vous auriez à écrire votre propre code thread-safe pour réaliser cela. En raison des initialisations supplémentaires exigées par une propriété en lecture/écriture stockée dans un objet Lazy<T>, les performances peuvent ne pas être acceptables. De plus, selon le scénario, une coordination supplémentaire peut être nécessaire pour éviter des conditions de concurrence entre les méthodes setter et getter.

Initialisation tardive de thread local

Dans certains scénarios multithreads, vous pouvez souhaiter que chaque thread ait ses propres données privées. Ces données sont appelées données de thread local. Dans le .NET Framework 3.5 et versions antérieures, vous pouvez appliquer l’attribut ThreadStatic à une variable statique pour qu’elle devienne une variable de thread local. Toutefois, l’utilisation de l’attribut ThreadStatic peut entraîner des erreurs subtiles. Par exemple, même avec des instructions d’initialisation basiques, la variable est initialisée uniquement sur le premier thread qui y accède, comme indiqué dans l’exemple suivant.

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

Sur tous les autres threads, la variable est initialisée à l’aide de sa valeur par défaut (zéro). En guise d’alternative dans le .NET Framework version 4, vous pouvez utiliser le type System.Threading.ThreadLocal<T> pour créer une variable de thread local basée sur l’instance, qui est initialisée sur tous les threads par le délégué Action<T> que vous fournissez. Dans l’exemple suivant, tous les threads qui accèdent à counter vont voir que sa valeur de départ est égale à 1.

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

ThreadLocal<T> encapsule son objet de la même façon que Lazy<T>, avec toutefois ces différences essentielles :

  • Chaque thread initialise la variable de thread local à l’aide de ses données privées, qui ne sont pas accessibles par d’autres threads.

  • La propriété ThreadLocal<T>.Value est en lecture-écriture et peut être modifiée autant de fois que nécessaire. Cela peut affecter la propagation des exceptions. Par exemple, une opération get peut lever une exception, mais celle qui suit peut initialiser la valeur.

  • Si aucun délégué d’initialisation n’est fourni, ThreadLocal<T> va initialiser son type encapsulé à l’aide de la valeur par défaut du type. À cet égard, ThreadLocal<T> est cohérent avec l’attribut ThreadStaticAttribute.

L’exemple suivant montre que chaque thread qui accède à l’instance ThreadLocal<int> obtient sa propre copie des données.

// 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 de thread local dans Parallel.For et ForEach

Lorsque vous utilisez la méthode Parallel.For ou Parallel.ForEach pour parcourir des sources de données en parallèle, vous pouvez utiliser les surcharges qui ont une prise en charge intégrée pour les données de thread local. Dans ces méthodes, pour obtenir des données de thread local, vous devez utiliser des délégués locaux pour créer ces données, y accéder et les nettoyer. Pour plus d’informations, consultez Guide pratique pour écrire une boucle Parallel.For avec des variables locales de thread et Guide pratique pour écrire une boucle Parallel.ForEach avec des variables locales de partition.

Utilisation de l’initialisation tardive pour les scénarios de faible charge

Dans les scénarios où vous devez initialiser tardivement un grand nombre d’objets, vous pouvez décider que l’encapsulation de chaque objet dans un Lazy<T> nécessite trop de mémoire ou trop de ressources informatiques. Vous pouvez aussi avoir des exigences strictes sur la façon dont l’initialisation tardive est exposée. Dans ce cas, vous pouvez utiliser les méthodes static (Shared en Visual Basic) de la classe System.Threading.LazyInitializer pour initialiser tardivement chaque objet sans l’encapsuler dans une instance de Lazy<T>.

Dans l’exemple suivant, supposons que, au lieu d’encapsuler un objet Orders entier dans un objet Lazy<T>, vous n’avez que des objets Order initialisés tardivement seulement lorsqu’ils sont nécessaires.

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

Dans cet exemple, notez que la procédure d’initialisation est appelée sur chaque itération de la boucle. Dans les scénarios multithreads, le premier thread à appeler la procédure d’initialisation est celui dont la valeur est visible par tous les threads. Les threads suivants appellent également la procédure d’initialisation, mais leurs résultats ne sont pas utilisés. Si ce type de condition de concurrence potentielle n’est pas acceptable, utilisez la surcharge de LazyInitializer.EnsureInitialized qui accepte un argument booléen et un objet de synchronisation.

Voir aussi