Meilleures pratiques pour le threading managé

Le multithreading exige une programmation attentive. Vous pouvez réduire la complexité pour la plupart des tâches en mettant les demandes en file d'attente pour une exécution par un thread du pool. Cette rubrique traite des situations les plus difficiles telles que la coordination du travail de plusieurs threads ou la gestion des threads qui bloquent.

RemarqueRemarque

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

Interblocages et conditions de concurrence critique

Le multithreading résout les problèmes de débit et de réactivité, mais ce faisant il introduit de nouveaux problèmes : les interblocages et les conditions de concurrence critique.

Interblocages

Un interblocage se produit lorsque l'un des deux threads essaie de verrouiller une ressource que l'autre thread a déjà verrouillée. Aucun thread ne peut avancer.

De nombreuses méthodes des classes de thread managé fournissent des délais pour vous aider à détecter des interblocages. Par exemple, le code suivant tente d'acquérir un verrou sur l'instance actuelle. Si ce verrou n'est pas obtenu en 300 millisecondes, Monitor.TryEnter retourne false.

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

Conditions de concurrence critique

Une condition de concurrence critique est une bogue qui se produit lorsque le résultat d'un programme dépend du premier des threads, parmi au moins deux threads, qui atteint un bloc de code particulier. L'exécution répétée du programme produit des résultats différents, et il est impossible de prédire le résultat d'une exécution donnée.

Un exemple simple de condition de concurrence critique est l'incrémentation d'un champ. Supposons qu'une classe possède un champ static privé (Shared en Visual Basic) qui est incrémenté à chaque création d'une instance de la classe à l'aide de code tel que objCt++; (C#) ou objCt += 1 (Visual Basic). Cette opération nécessite le chargement dans un registre de la valeur à partir de objCt, l'incrémentation de cette valeur et son stockage dans objCt.

Dans une application multithread, un thread qui a chargé et incrémenté la valeur peut être interrompu 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é entre-temps.

Cette condition de concurrence critique particulière est évitée facilement en utilisant des méthodes de la classe Interlocked, comme Interlocked.Increment. Pour plus d'informations sur d'autres techniques permettant de synchroniser des données entre plusieurs threads, consultez Synchronisation des données pour le multithreading.

Les conditions de concurrence critique peuvent également se produire lorsque vous synchronisez les activités de plusieurs threads. Chaque fois que vous écrivez une ligne de code, vous devez anticiper ce qui peut se produire si un thread était interrompu avant l'exécution de la ligne (ou avant les instructions machine individuelles qui composent la ligne), et qu'un autre thread prenne le relais.

Nombre de processeurs

Le multithreading résout différents problèmes pour les ordinateurs à un seul processeur qui exécutent l'essentiel des logiciels de l'utilisateur final, et les ordinateurs multiprocesseurs utilisés généralement comme serveurs.

Ordinateurs à processeurs uniques

Le multithreading fournit une plus grande réactivité à l'utilisateur de l'ordinateur et utilise le temps d'inactivité pour les tâches en arrière-plan. Si vous utilisez le multithreading sur un ordinateur à processeur unique :

  • Seul un thread s'exécute à tout moment.

  • Un thread en arrière-plan s'exécute uniquement lorsque le thread utilisateur principal est inactif. Un thread en premier plan qui s'exécute constamment prive les threads en arrière-plan de temps processeur.

  • Lorsque vous appelez la méthode Thread.Start sur un thread, ce thread ne s'exécute pas tant que le thread en cours ne cède pas ou n'est pas interrompu par le système d'exploitation.

  • Les conditions de concurrence critique se produisent généralement parce que le programmeur n'a pas anticipé le fait qu'un thread peut être interrompu à un moment délicat, ce qui permet parfois à un autre thread de parvenir au bloc de code le premier.

Ordinateurs multiprocesseurs

Le multithreading fournit un débit supérieur. Dix processeurs peuvent effectuer dix fois plus de travail qu'un seul processeur, mais uniquement si le travail est divisé de telle sorte que les dix processeurs puissent travailler simultanément ; les threads fournissent un moyen facile de diviser le travail et de tirer parti de la puissance de traitement supplémentaire. Si vous utilisez le multithreading sur un ordinateur multiprocesseur :

  • Le nombre de threads qui peuvent s'exécuter simultanément est limité par le nombre de processeurs.

  • Un thread d'arrière-plan s'exécute uniquement lorsque le nombre de threads de premier plan est inférieur au nombre de processeurs.

  • Lorsque vous appelez la méthode Thread.Start sur un thread, ce thread peut ou ne peut pas démarrer son exécution immédiatement selon le nombre de processeurs et le nombre de threads qui attendent actuellement de s'exécuter.

  • Les conditions de concurrence critique peuvent se produire non seulement du fait de l'interruption inattendue des threads mais parce que deux threads qui s'exécutent sur des processeurs différents s'acheminent peut-être vers le même bloc de code.

Membres statiques et constructeurs statiques

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

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

Cela s'applique à tout type pouvant disposer d'un constructeur static.

Recommandations générales

Observez les recommandations suivantes lors de l'utilisation de plusieurs threads :

  • N'utilisez pas Thread.Abort pour mettre fin à d'autres threads. L'appel à Abort sur un autre thread revient à lever une exception sur ce thread, sans savoir à quel point ce thread est parvenu 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 (en utilisant des événements, par exemple). À la place, concevez votre programme de telle sorte que les threads de travail soient chargés d'attendre que du travail soit disponible, d'exécuter ce travail et de notifier les autres parties de votre programme une fois terminé. Si vos threads de travail ne provoquent pas de blocage, utilisez des threads du pool de threads. Monitor.PulseAll est utile dans les cas où les threads de travail se bloquent.

  • N'utilisez pas de types comme objets lock. Autrement dit, évitez les codes tels que lock(typeof(X)) en C# ou SyncLock(GetType(X)) dans Visual Basic, ou l'utilisation de Monitor.Enter avec des objets Type. Pour un type donné, il existe une seule instance de System.Type par domaine d'application. Si le type sur lequel vous acquérez un verrou est public, tout code, autre que le vôtre, peut acquérir des verrous sur celui-ci, ce qui entraîne des interblocages. Pour plus d'informations, consultez Meilleures pratiques pour la fiabilité.

  • Soyez vigilants lors du verrouillage d'instances, par exemple lock(this) en C# ou SyncLock(Me) dans Visual Basic. Si un autre code de votre application, externe au type, acquiert un verrou sur l'objet, des interblocages peuvent se produire.

  • Veillez à ce qu'un thread qui est entré dans un moniteur quitte toujours ce moniteur, même si une exception se produit tandis que le thread se trouve dans le moniteur. L'instruction C# lock et l'instruction Visual Basic SyncLock fournissent ce comportement automatiquement en se servant d'un bloc finally pour garantir l'appel à Monitor.Exit. Si vous ne pouvez pas garantir l'appel à Exit, envisagez de changer votre design afin d'utiliser Mutex. Un mutex est libéré automatiquement lorsque le thread qui le possède prend fin.

  • Utilisez plusieurs threads pour des tâches qui nécessitent des ressources différentes, et évitez d'assigner plusieurs threads à une seule ressource. Par exemple, toutes les tâches d'E/S ont intérêt à posséder leur propre thread, car ce thread bloquera lors des opérations d'E/S empêchant ainsi l'exécution d'autres threads. Les entrées d'utilisateur sont une autre ressource qui tire profit d'un thread dédié. Sur un ordinateur à un seul processeur, une tâche qui implique des calculs intensifs cohabite avec les entrées d'utilisateur et avec des tâches d'E/S, mais les tâches consommant beaucoup de calcul entrent en concurrence.

  • Envisagez l'utilisation des méthodes de la classe Interlocked pour les modifications d'état simples, plutôt que l'instruction lock (SyncLock en Visual Basic). L'instruction lock est un bon outil à usage général, mais la classe Interlocked fournit de meilleures performances pour les mises à jour qui doivent être atomiques. En interne, il exécute un préfixe de verrouillage seul s'il n'y a aucun conflit. Lors de la révision du code, recherchez le code correspondant à celui 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 méthode Increment au lieu de l'instruction lock, comme suit :

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

    Dans le .NET Framework version 2.0, la méthode Add fournit des mises à jour atomiques dans les incréments 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)
        {
            if (x == null)
            {
                x = y;
            }
        }
    }
    

    Les performances peuvent être améliorées en utilisant à la place la méthode CompareExchange, comme suit :

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

    Dans le .NET Framework version 2.0, la méthode CompareExchange a une surcharge générique qui peut être utilisée pour le remplacement de type sécurisé de tout type référence.

Recommandations pour les bibliothèques de classes

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

  • Évitez le besoin de synchronisation, si possible. C'est particulièrement vrai dans le cas de code très utilisé. Par exemple, un algorithme peut être ajusté pour tolérer une condition de concurrence critique plutôt que de l'éliminer. Une synchronisation inutile diminue les performances et entraîne des possibilités d'interblocage et de conditions de concurrence critique.

  • Rendez les données statiques (Shared en Visual Basic) thread-safe par défaut.

  • Ne rendez pas les données d'instance thread-safe par défaut. L'ajout de verrous pour créer du code thread-safe diminue les performances, augmente les conflits de verrouillage et entraîne des possibilités 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 Framework 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é par plusieurs demandes, ce qui signifie que plusieurs threads peuvent exécuter ce code en même temps. Cela entraîne la possibilité de bogues liés aux threads. Envisagez d'utiliser un modèle de design qui encapsule des données dans des instances qui ne sont pas partagées par plusieurs demandes. En outre, si les données statiques sont synchronisées, les appels entre les méthodes statiques qui altèrent l'état peuvent entraîner des interblocages ou une synchronisation redondante, ce qui a une incidence néfaste sur les performances.

Voir aussi

Concepts

Threads et threading

Autres ressources

Threading managé