Partager via


Bonnes pratiques pour le threading managé

Le multithreading nécessite une programmation minutieuse. Pour réduire la complexité de la plupart des tâches, il vous suffit de mettre en file d’attente les requêtes à exécuter par les threads d’un pool de threads. Cette rubrique aborde des situations plus difficiles, telles que la coordination du travail de plusieurs threads ou la gestion des threads qui bloquent.

Remarque

À compter de .NET Framework 4, la bibliothèque parallèle de tâches et PLINQ fournissent des API qui réduisent la complexité et les risques de la programmation multithread. Pour plus d’informations, consultez Programmation parallèle dans .NET.

Interblocages et conditions de concurrence

Le multithreading résout les problèmes liés au débit et à la réactivité, mais, dans ce cas, il introduit de nouveaux problèmes : blocages et conditions de concurrence.

Blocages

Un interblocage se produit lorsque chacun des deux threads tente de verrouiller une ressource que l’autre a déjà verrouillée. Aucun thread ne peut progresser davantage.

De nombreuses méthodes des classes de threading managé fournissent des délais d’expiration conçus pour faciliter la détection des interblocages. Par exemple, le code suivant tente d’acquérir un verrou sur un objet nommé lockObject. Si le verrou n’est pas obtenu en 300 millisecondes, Monitor.TryEnter retourne false.

If Monitor.TryEnter(lockObject, 300) Then
    Try
        ' Place code protected by the Monitor here.
    Finally
        Monitor.Exit(lockObject)
    End Try
Else
    ' Code to execute if the attempt times out.
End If
if (Monitor.TryEnter(lockObject, 300)) {
    try {
        // Place code protected by the Monitor here.
    }
    finally {
        Monitor.Exit(lockObject);
    }
}
else {
    // Code to execute if the attempt times out.
}

Conditions de course

Une condition de concurrence est un bogue qui se produit lorsque le résultat d’un programme dépend de laquelle deux threads ou plus atteignent d’abord un bloc de code particulier. L’exécution du programme produit plusieurs fois des résultats différents et le résultat d’une exécution donnée ne peut pas être prédit.

Un exemple simple de condition de concurrence correspond à l’incrémentation d’un champ. Supposons qu’une classe a un champ statique privé (partagé en Visual Basic) qui est incrémenté chaque fois qu’une instance de la classe est créée, à l’aide de code tel que objCt++; (C#) ou objCt += 1 (Visual Basic). Cette opération nécessite le chargement de la valeur de objCt dans un registre, l’incrémentation de la valeur et son stockage dans objCt.

Dans une application multithread, un thread qui a chargé et incrémenté la valeur peut être préempté par un autre thread qui effectue les trois étapes ; lorsque le premier thread reprend l’exécution et stocke sa valeur, il remplace objCt sans tenir compte du fait que la valeur a changé dans l’intervalle.

Cette condition de concurrence particulière est facile à éviter en utilisant des méthodes de la classe Interlocked, telles que Interlocked.Increment. Pour en savoir plus sur les autres techniques de synchronisation des données entre plusieurs threads, consultez Synchronizing Data for Multithreading.

Des conditions de concurrence peuvent également survenir lorsque vous synchronisez les activités de plusieurs threads. Chaque fois que vous écrivez une ligne de code, vous devez prendre en compte ce qui peut se produire si un thread a été préempté avant d’exécuter la ligne (ou avant l’une des instructions de machine individuelles qui composent la ligne), et un autre thread l’a dépassé.

Membres statiques et constructeurs statiques

Une classe n’est pas initialisée tant que son constructeur de classe (static constructeur en C#, Shared Sub New en Visual Basic) n’est pas en cours d’exécution. Pour empêcher l’exécution du code sur un type qui n’est pas initialisé, le Common Language Runtime bloque tous les appels d’autres threads aux membres static de la classe (Shared membres en Visual Basic) jusqu’à ce que le constructeur de la classe ait terminé son exécution.

Par exemple, si un constructeur de classe démarre un nouveau thread et que la procédure thread appelle un static membre de la classe, le nouveau thread bloque jusqu’à ce que le constructeur de classe se termine.

Cela s’applique à tout type pouvant avoir un constructeur static.

Nombre de processeurs

Qu’il existe plusieurs processeurs ou qu’un seul processeur soit disponible sur un système peut influencer l’architecture multithread. Pour plus d’informations, consultez Nombre de processeurs.

Utilisez la Environment.ProcessorCount propriété pour déterminer le nombre de processeurs disponibles au moment de l’exécution.

Recommandations générales

Tenez compte des instructions suivantes lors de l’utilisation de plusieurs threads :

  • N’utilisez Thread.Abort pas pour arrêter d’autres threads. L’appel Abort sur un autre thread est semblable à la levée d’une exception sur ce thread, sans savoir quel point ce thread a atteint dans son traitement.

  • N’utilisez pas Thread.Suspend et Thread.Resume pour synchroniser les activités de plusieurs threads. Utilisez Mutex, ManualResetEvent, AutoResetEvent et Monitor.

  • Ne contrôlez pas l’exécution des threads de travail à partir de votre programme principal (à l’aide d’événements, par exemple). Au lieu de cela, concevez votre programme afin que les threads de travail soient chargés d’attendre que le travail soit disponible, de l’exécuter et de notifier d’autres parties de votre programme une fois terminé. Si vos threads de travail ne se bloquent pas, envisagez d’utiliser les threads d’un pool de threads. Monitor.PulseAll est utile dans les situations où les threads de travail se bloquent.

  • N’utilisez pas de types comme objets de verrou. Autrement dit, évitez d'utiliser du code comme «lock(typeof(X))» en C# ou «SyncLock(GetType(X))» en Visual Basic, ou encore l’utilisation d’objets avec «Monitor.Enter» et «Type». Pour un type donné, il n’existe qu’une seule instance de System.Type par domaine d’application. Si le type sur lequel vous utilisez un verrou est public, un code différent du vôtre peut utiliser des verrous sur ce type, entraînant ainsi des interblocages. Pour d’autres problèmes, consultez Meilleures pratiques en matière de fiabilité.

  • Utilisez une prudence lors du verrouillage sur des instances, par exemple lock(this) en C# ou SyncLock(Me) en Visual Basic. Si d'autres segments de code de votre application, externes au type, verrouillent l'objet, des interblocages peuvent se produire.

  • Assurez-vous qu’un thread entré dans un moniteur quitte toujours ce moniteur, même si une exception se produit pendant que le thread se trouve dans le moniteur. L’instruction lock en C# et l’instruction SyncLock en Visual Basic fournissent ce comportement automatiquement, en utilisant un bloc finally pour garantir que Monitor.Exit est appelé. Si vous ne pouvez pas vous assurer que Exit sera appelée, envisagez de modifier votre conception pour utiliser Mutex. Un mutex est automatiquement libéré lorsque le thread qui le possède actuellement se termine.

  • Utilisez plusieurs threads pour les tâches nécessitant différentes ressources et évitez d’affecter plusieurs threads à une seule ressource. Par exemple, toute tâche impliquant des E/S bénéficie d’avoir son propre thread, car ce thread bloque pendant les opérations d’E/S et permet ainsi à d’autres threads d’être exécutés. L’entrée utilisateur est une autre ressource qui bénéficie d’un thread dédié. Sur un ordinateur à processeur unique, une tâche qui implique un calcul intensif coexiste avec les entrées utilisateur et les tâches qui impliquent des E/S, mais plusieurs tâches nécessitant beaucoup de calcul s'opposent les unes aux autres.

  • Envisagez d’utiliser des méthodes de la Interlocked classe pour des modifications d’état simples, au lieu d’utiliser l’instruction lock (SyncLock en Visual Basic). L’instruction lock est un bon outil à usage général, mais la Interlocked classe offre de meilleures performances pour les mises à jour qui doivent être atomiques. En interne, il exécute un préfixe de verrouillage unique s'il n'y a pas de conflit. Dans les révisions de code, regardez le code comme illustré dans les exemples suivants. Dans le premier exemple, une variable d’état est incrémentée :

    SyncLock lockObject
        myField += 1
    End SyncLock
    
    lock(lockObject)
    {
        myField++;
    }
    

    Vous pouvez améliorer les performances à l’aide de la Increment méthode au lieu de l’instruction lock , comme suit :

    System.Threading.Interlocked.Increment(myField)
    
    System.Threading.Interlocked.Increment(myField);
    

    Remarque

    Utilisez la Add méthode pour les incréments atomiques supérieurs à 1.

    Dans le deuxième exemple, une variable de type référence est mise à jour uniquement s’il s’agit d’une référence Null (Nothing en Visual Basic).

    If x Is Nothing Then
        SyncLock lockObject
            If x Is Nothing Then
                x = y
            End If
        End SyncLock
    End If
    
    if (x == null)
    {
        lock (lockObject)
        {
            x ??= y;
        }
    }
    

    Les performances peuvent être améliorées en utilisant plutôt la méthode CompareExchange, comme suit :

    System.Threading.Interlocked.CompareExchange(x, y, Nothing)
    
    System.Threading.Interlocked.CompareExchange(ref x, y, null);
    

    Remarque

    La surcharge de méthode CompareExchange<T>(T, T, T) fournit une alternative de type sécurisée pour les types de référence.

Recommandations pour les bibliothèques de classes

Tenez compte des instructions suivantes lors de la conception de bibliothèques de classes pour le multithreading :

  • Évitez d’avoir besoin de synchronisation, si possible. Cela est particulièrement vrai pour le code fortement utilisé. Par exemple, un algorithme peut être ajusté pour tolérer une condition de race plutôt que de l’éliminer. La synchronisation inutile réduit les performances et crée la possibilité d’interblocages et de conditions de compétition.

  • Faites en sorte que les données statiques (Shared en Visual Basic) soient sécurisées par défaut.

  • Ne pas rendre le thread de données d’instance sécurisé par défaut. L’ajout de verrous pour créer du code thread-safe diminue les performances, augmente la contention des verrous et crée la possibilité d’interblocages. Dans les modèles d’application courants, un seul thread à la fois exécute le code utilisateur, ce qui réduit le besoin de sécurité des threads. Pour cette raison, les bibliothèques de classes .NET ne sont pas thread safe par défaut.

  • Évitez de fournir des méthodes statiques qui modifient l’état statique. Dans les scénarios de serveur courants, l’état statique est partagé entre les requêtes, ce qui signifie que plusieurs threads peuvent exécuter ce code en même temps. Ceci occasionne un risque de bogues de threading. Envisagez d’utiliser un modèle de conception qui encapsule des données dans des instances qui ne sont pas partagées entre les requêtes. En outre, si les données statiques sont synchronisées, les appels entre les méthodes statiques qui modifient l’état peuvent entraîner des blocages ou une synchronisation redondante, ce qui affecte négativement les performances.

Voir aussi