Partager via


Synchronisation des threads (C# et Visual Basic)

Les sections suivantes décrivent les fonctionnalités et classes qui peuvent être utilisées pour synchroniser l'accès aux ressources dans les applications multithread.

L'un des avantages de l'utilisation de plusieurs threads dans une application est que chaque thread s'exécute de façon asynchrone. Pour les applications Windows, cela permet d'exécuter en arrière-plan des tâches longues pendant que la fenêtre d'application et les contrôles restent réactifs. Pour les applications serveur, le multithreading fournit la capacité de gérer chaque requête entrante avec un thread différent. Sinon, chaque nouvelle demande n'est pas prise en charge tant que la demande précédente n'est pas complètement satisfaite.

Toutefois, la nature asynchrone des threads a pour conséquence que l'accès aux ressources telles que les handles de fichiers, les connexions réseau et la mémoire doit être coordonné. Sinon, deux ou plusieurs threads pourraient accéder en même temps à la même ressource, chacun ignorant les actions des autres. Il en résulte des données endommagées imprévisibles.

Pour les opérations simples sur les types de données numériques intégrales, la synchronisation des threads peut se faire avec les membres de la classe Interlocked. Pour tous les autres types de données et les ressources non thread-safe, le multithreading peut se faire sans risque uniquement à l'aide des constructions présentées dans cette rubrique.

Pour obtenir des informations générales sur la programmation multithread, consultez :

Mots clés lock et SyncLock

Les instructions lock (C#) et SyncLock (Visual Basic) peuvent être utilisées pour garantir qu'un bloc de code soit exécuté jusqu'à la fin, sans interruption par d'autres threads. Cela se fait en obtenant un verrouillage d'exclusion mutuelle pour un objet donné pour la durée du bloc de code.

Une instruction lock ou SyncLock obtient un objet comme argument et est suivie par un bloc de code qui est exécuté par un seul thread à la fois. Par exemple :

Public Class TestThreading
    Dim lockThis As New Object 

    Public Sub Process()
        SyncLock lockThis
            ' Access thread-sensitive resources. 
        End SyncLock 
    End Sub 
End Class
public class TestThreading
{
    private System.Object lockThis = new System.Object();

    public void Process()
    {

        lock (lockThis)
        {
            // Access thread-sensitive resources.
        }
    }

}

L'argument fourni au mot clé lock doit être un objet basé sur un type référence ; il est utilisé pour définir la portée du verrouillage. Dans l'exemple précité, la portée de verrouillage est limitée à cette fonction parce qu'aucune référence à l'objet lockThis n'existe en dehors de la fonction. Si une telle référence existait réellement, la portée de verrouillage s'étendrait à cet objet. Au sens strict, l'objet fourni est utilisé uniquement pour identifier de manière unique la ressource qui est partagée par plusieurs threads. Il peut donc s'agir d'une instance de classe arbitraire. Dans la pratique, toutefois, cet objet représente généralement la ressource pour laquelle la synchronisation de threads est nécessaire. Par exemple, si un objet conteneur doit être utilisé par plusieurs threads, le conteneur peut être passé pour verrouiller, et le bloc de code synchronisé suivant le verrouillage accéderait au conteneur. Tant que d'autres threads sont verrouillés sur le même conteneur avant d'y accéder, l'accès à l'objet est synchronisé sans risque.

En règle générale, il est préférable d'éviter de verrouiller sur un type public, ou sur des instances d'objets que votre application ne contrôle pas. Par exemple, lock(this) peut poser des problèmes s'il est possible d'accéder publiquement à l'instance, car du code que vous ne contrôlez pas peut également se verrouiller sur l'objet. Cela pourrait créer des situations d'interblocage dans lesquelles deux ou plusieurs threads attendent la libération du même objet. Le verrouillage sur un type de données public, par opposition à un objet, peut entraîner des problèmes pour la même raison. Le verrouillage sur des chaînes littérales est particulièrement risqué, car les chaînes littérales sont internées par le common language runtime (CLR). Cela signifie qu'il existe une instance de tous les littéraux de chaîne pour le programme entier ; le même objet représente le littéral dans tous les domaines d'application en cours d'exécution, sur tous les threads. En conséquence, un verrouillage placé n'importe où sur une chaîne avec le même contenu dans le processus d'application enferme toutes les instances de cette chaîne dans l'application. En conséquence, il est préférable de verrouiller un membre privé ou protégé qui n'est pas interné. Certaines classes fournissent spécifiquement des membres destinés au verrouillage. Par exemple, le type Array fournit SyncRoot. Beaucoup de types de collection fournissent également un membre SyncRoot.

Pour plus d'informations sur les instructions lock et SyncLock, consultez les rubriques suivantes :

Moniteurs

Comme les mots clés lock et SyncLock, les moniteurs empêchent des blocs de code d'être exécutés simultanément par plusieurs threads. La méthode Enter permet à un seul thread de continuer dans les instructions suivantes ; tous les autres threads sont bloqués jusqu'à ce que le thread exécutant appelle Exit. Cela équivaut à utiliser le mot clé lock. Par exemple :

SyncLock x
    DoSomething()
End SyncLock
lock (x)
{
    DoSomething();
}

Ceci équivaut à :

Dim obj As Object = CType(x, Object)
System.Threading.Monitor.Enter(obj)
Try
    DoSomething()
Finally
    System.Threading.Monitor.Exit(obj)
End Try
System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
    DoSomething();
}
finally
{
    System.Threading.Monitor.Exit(obj);
}

L'utilisation du mot clé lock (C#) ou SyncLock (Visual Basic) est généralement privilégiée par rapport à l'utilisation de la classe Monitor directement, car lock ou SyncLock est plus concis, et parce que lock ou SyncLock assure que le moniteur sous-jacent est libéré, même si le code protégé lève une exception. Cela se fait à l'aide du mot clé finally, qui exécute son bloc de code associé, qu'une exception soit levée ou non.

Événements de synchronisation et handles d'attente

L'utilisation d'un verrouillage ou d'un moniteur est utile pour empêcher l'exécution simultanée de blocs de code sensibles aux threads, mais ces constructions ne permettent pas à un thread de communiquer un événement à un autre. Cela nécessite des événements de synchronisation, qui sont des objets ayant l'un de deux états, signalé et non signalé, qui permettent d'activer et d'interrompre des threads. Les threads peuvent être interrompus en étant obligés d'attendre un événement de synchronisation qui n'est pas signalé et peuvent être activés en modifiant l'état d'événement à signalé. Si un thread tente d'attendre un événement qui est déjà signalé, le thread continue de s'exécuter sans délai.

Il existe deux types d'événements de synchronisation : AutoResetEvent, et ManualResetEvent. Ils diffèrent uniquement dans la mesure où AutoResetEvent change d'un état signalé à non signalé automatiquement à chaque fois qu'il active un thread. À l'inverse, un ManualResetEvent permet l'activation d'un certain nombre de threads par leur état signalé et revient à un état non signalé uniquement lorsque sa méthode Reset est appelée.

Il est possible d'obliger les threads à attendre les événements en appelant l'une des méthodes d'attente, telles que WaitOne, WaitAny ou WaitAll. WaitHandle.WaitOne provoque l'attente du thread jusqu'à ce qu'un événement unique soit signalé, WaitHandle.WaitAny bloque un thread jusqu'à ce qu'un ou plusieurs événements indiqués soient signalés, et WaitHandle.WaitAll bloque le thread jusqu'à ce que tous les événements indiqués soient signalés. Un événement est signalé lorsque sa méthode Set est appelée.

Dans l'exemple suivant, un thread est créé et démarré par la fonction Main. Le nouveau thread attend un événement à l'aide de la méthode WaitOne. Le thread est interrompu jusqu'à ce que l'événement soit signalé par le thread principal qui exécute la fonction Main. Une fois que l'événement est signalé, le thread auxiliaire est retourné. Dans ce cas, parce que l'événement est utilisé uniquement pour une activation de thread, les classes AutoResetEvent ou ManualResetEvent pourraient être utilisées.

Imports System.Threading

Module Module1
    Dim autoEvent As AutoResetEvent

    Sub DoWork()
        Console.WriteLine("   worker thread started, now waiting on event...")
        autoEvent.WaitOne()
        Console.WriteLine("   worker thread reactivated, now exiting...")
    End Sub 

    Sub Main()
        autoEvent = New AutoResetEvent(False)

        Console.WriteLine("main thread starting worker thread...")
        Dim t As New Thread(AddressOf DoWork)
        t.Start()

        Console.WriteLine("main thread sleeping for 1 second...")
        Thread.Sleep(1000)

        Console.WriteLine("main thread signaling worker thread...")
        autoEvent.Set()
    End Sub 
End Module
using System;
using System.Threading;

class ThreadingExample
{
    static AutoResetEvent autoEvent;

    static void DoWork()
    {
        Console.WriteLine("   worker thread started, now waiting on event...");
        autoEvent.WaitOne();
        Console.WriteLine("   worker thread reactivated, now exiting...");
    }

    static void Main()
    {
        autoEvent = new AutoResetEvent(false);

        Console.WriteLine("main thread starting worker thread...");
        Thread t = new Thread(DoWork);
        t.Start();

        Console.WriteLine("main thread sleeping for 1 second...");
        Thread.Sleep(1000);

        Console.WriteLine("main thread signaling worker thread...");
        autoEvent.Set();
    }
}

Objet Mutex

Un mutex est semblable à un moniteur ; il empêche l'exécution simultanée d'un bloc de code par plusieurs threads. En fait, le nom « mutex » est la contraction de « mutually exclusive » (qui s'excluent mutuellement). Cependant, à la différence d'un moniteur, un mutex peut être utilisé pour synchroniser des threads dans plusieurs processus. Un mutex est représenté par la classe Mutex.

Dans le cadre d'une synchronisation inter-processus, un mutex est appelé mutex nommé parce qu'il sera utilisé dans une autre application, et que par conséquent il ne peut pas être partagé au moyen d'une variable globale ou statique. Il est indispensable de lui attribuer un nom, afin que les deux applications puissent accéder au même objet mutex.

Bien qu'un mutex puisse être utilisé pour la synchronisation de threads intra-processus, l'utilisation de Monitor est généralement privilégiée, car les moniteurs ont été conçus spécifiquement pour .NET Framework et par conséquent utilisent les ressources plus efficacement. La classe Mutex en revanche est un wrapper d'une construction Win32. Tout en étant plus puissant qu'un moniteur, un mutex nécessite des transitions d'interopérabilité qui demandent plus de calculs que celles requises par la classe Monitor. Pour obtenir un exemple de l'utilisation d'un mutex, consultez Mutex.

Classe Interlocked

Vous pouvez utiliser les méthodes de la classe Interlocked pour éviter les problèmes qui peuvent se produire lorsque plusieurs threads tentent simultanément de mettre à jour ou de comparer une même valeur. Les méthodes de cette classe vous permettent d'incrémenter, de décrémenter, d'échanger et de comparer des valeurs d'un thread en toute sécurité.

Verrous ReaderWriter

Dans certains cas, vous pouvez souhaiter verrouiller une ressource uniquement lorsque les données sont en cours d'écriture et autoriser plusieurs clients à lire simultanément des données qui ne sont pas en cours de mises à jour. La classe ReaderWriterLock met en œuvre l'accès exclusif à une ressource en cours de modification par un thread, mais elle permet d'accéder en mode non exclusif à cette ressource pour la lire. Les verrous ReaderWriter constituent une alternative utile aux verrous exclusifs qui provoquent l'attente des autres threads, même lorsque ces threads n'ont pas besoin de mettre à jour des données.

Interblocages

La synchronisation des threads est d'une aide inestimable dans les applications multithread, mais le risque de création d'un deadlock subsiste lorsque plusieurs threads s'attendent mutuellement et que l'application s'arrête. On peut comparer l'interblocage au cas d'automobiles bloquées à un carrefour où chaque conducteur attend que l'autre passe. Il est important d'éviter les interblocages, la solution étant une planification soigneuse. Vous pouvez souvent prévoir les situations d'interblocage à l'aide d'un schéma des applications multithread élaboré avant le démarrage du codage.

Rubriques connexes

Comment : utiliser un pool de threads (C# et Visual Basic)

Comment : synchroniser l'accès à une ressource partagée dans un environnement de multithreading à l'aide de Visual C# .NET

Comment : créer un thread à l'aide de Visual C# .NET (en anglais)

Comment : envoyer un élément de travail dans le pool de threads à l'aide de Visual C# .NET

Comment : synchroniser l'accès à une ressource partagée dans un environnement de multithreading à l'aide de Visual C# .NET

Voir aussi

Référence

SyncLock, instruction

lock, instruction (référence C#)

Thread

WaitOne

WaitAny

WaitAll

Join

Start

Sleep

Monitor

Mutex

AutoResetEvent

ManualResetEvent

Interlocked

WaitHandle

EventWaitHandle

System.Threading

Set

Concepts

Applications multithread (C# et Visual Basic)

Mutex

Moniteurs

Opérations verrouillées

AutoResetEvent

Synchronisation des données pour le multithreading

Autres ressources

Implémentation du modèle de programmation asynchrone du CLR

APM simplifié avec C#

Moniteur d'interblocage

Multithreading dans les composants

Procédure : Synchroniser l'accès à une ressource partagée dans un environnement multithread à l'aide de Visual C# .NET