Synchronisation de threads (Guide de programmation C#)

Mise à jour : novembre 2007

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 :

Le mot clé lock

Le mot clé lock permet de garantir qu'un bloc de code s'exécute jusqu'à la fin sans être interrompu 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 commence par le mot clé lock, auquel un objet est donné comme argument, suivi d'un bloc de code qui sera exécuté par un seul thread à la fois. Par exemple :

public class TestThreading
{
    private System.Object lockThis = new System.Object();

    public void Function()
    {

        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 à lock 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 de blocage 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 le mot clé lock, consultez :

Moniteurs

Comme le mot clé lock, 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. En fait, le mot clé lock est implémenté avec la classe Monitor. Par exemple :

lock (x)
{
    DoSomething();
}

Ceci équivaut à :

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 est généralement privilégiée par rapport à l'utilisation de la classe Monitor directement, car lock est plus concis, et parce que lock 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.

Pour plus d'informations sur les moniteurs, consultez Synchronisation de moniteurs, exemple de technologie.

É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() fait en sorte que le thread attende qu'un événement 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.

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();
    }
}

Pour obtenir d'autres exemples d'utilisation d'événements de synchronisation de threads, consultez :

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.

Rubriques connexes

Voir aussi

Concepts

Guide de programmation C#

Référence

Thread

WaitOne

WaitAny

WaitAll

Monitor

Mutex

AutoResetEvent

ManualResetEvent

Interlocked

WaitHandle

Autres ressources

Implementing the CLR Asynchronous Programming Model

Simplified APM with C#

Deadlock monitor