Partager via


Vue d'ensemble des primitives de synchronisation

Mise à jour : novembre 2007

Le .NET Framework fournit une plage de primitives de synchronisation pour contrôler les interactions de threads et éviter des conditions de concurrence critique. Celles-ci peuvent être divisées approximativement en trois catégories : le verrouillage, la signalisation et les opérations verrouillées.

Les catégories ne sont pas clairement définies car certains mécanismes de synchronisation possèdent des caractéristiques relevant de plusieurs catégories : les événements qui libèrent un seul thread à la fois sont similaires à des verrous d'un point de vue fonctionnel, la libération d'un verrou quelconque peut être assimilée à un signal et les opérations verrouillées peuvent être utilisées pour construire des verrous. Toutefois, ces catégories sont utiles.

Il est important de se souvenir que la synchronisation de threads est par nature coopérative. Si un seul thread ignore un mécanisme de synchronisation et accède directement à la ressource protégée, ce mécanisme de synchronisation ne peut pas être efficace.

Verrouillage

Les verrous donnent le contrôle d'une ressource à un seul thread à la fois ou à un nombre spécifié de threads. Un thread qui demande un verrou exclusif lorsque le verrou est en cours d'utilisation se bloque jusqu'à ce que le verrou soit à nouveau disponible.

Verrous exclusifs

La forme la plus simple de verrouillage est l'instruction lock C# (SyncLock en Visual Basic) qui contrôle l'accès à un bloc de code. Un bloc de ce type est souvent désigné par le terme « section critique ». L'instruction lock est implémentée à l'aide des méthodes Enter et Exit de la classe Monitor et elle utilise try…catch…finally pour garantir la libération du verrou.

En général, l'utilisation de l'instruction lock pour protéger de petits blocs de code mais sans jamais englober plus d'une seule méthode représente le meilleur moyen d'utiliser la classe Monitor. Bien que puissante, la classe Monitor est susceptible de rendre des verrous et des blocages orphelins.

Classe Monitor

La classe Monitor fournit des fonctionnalités supplémentaires, lesquelles peuvent être utilisées conjointement à l'instruction lock:

  • La méthode TryEnter autorise un thread qui est bloqué en attente d'une ressource, d'abandonner après un intervalle spécifié. Elle retourne une valeur de type Boolean qui indique le succès ou l'échec. Vous pouvez utiliser ces informations pour détecter et éviter des blocages potentiels.

  • La méthode Wait est appelée par un thread dans une section critique. Elle abandonne le contrôle de la ressource et se bloque jusqu'à ce que la ressource soit à nouveau disponible.

  • Les méthodes Pulse et PulseAll permettent à un thread en passe de libérer le verrou ou d'appeler Wait de placer un ou plusieurs threads dans la file d'attente opérationnelle, afin qu'ils puissent acquérir le verrou.

Les délais d'attente sur les surcharges de la méthode Wait permettent aux threads en attente de se déplacer vers la file d'attente opérationnelle.

La classe Monitor peut fournir le verrouillage dans plusieurs domaines d'application si l'objet utilisé comme verrou dérive de MarshalByRefObject.

Monitor possède l'affinité de thread. En d'autres termes, un thread entré dans un moniteur doit sortir en appelant Exit ou Wait.

Il est impossible d'instancier la classe Monitor. Ses méthodes sont statiques (Shared en Visual Basic) et agissent sur un objet verrou qui, lui, peut être instancié.

Pour une vue d'ensemble conceptuelle, consultez Moniteurs.

Classe Mutex

Les threads demandent un Mutex en appelant une surcharge de sa méthode WaitOne. Des surcharges avec délais d'attente sont fournies, afin de permettre aux threads d'abandonner l'attente. À la différence de la classe Monitor, un mutex peut être local ou global. Les mutex globaux, appelés également mutex nommés, sont visibles dans tout le système d'exploitation et peuvent être utilisés pour synchroniser des threads dans plusieurs domaines d'application ou processus. Les mutex locaux dérivent de MarshalByRefObject et peuvent être utilisés au-delà des limites de domaine d'application.

De plus, Mutex dérive de WaitHandle, ce qui signifie qu'il peut être utilisé avec les mécanismes de signalisation fournis par WaitHandle, et notamment les méthodes WaitAll, WaitAny et SignalAndWait.

À l'instar de Monitor, Mutex possède l'affinité de thread. Contrairement à Monitor, il est possible d'instancier un objet Mutex.

Pour une vue d'ensemble conceptuelle, consultez Mutex.

Autres verrous

Les verrous n'ont pas besoin d'être exclusifs. Il est souvent utile d'autoriser un nombre limité d'accès concurrentiels de threads à une ressource. Les sémaphores et les verrous lecteur/writer sont conçus pour contrôler ce type d'accès aux ressources de pool.

Classe ReaderWriterLock

La classe ReaderWriterLockSlim intervient dans le cas où un thread qui modifie des données, le writer, doit disposer d'un accès exclusif à une ressource. Lorsque le writer n'est pas actif, un nombre quelconque de lecteurs peuvent accéder à la ressource (par exemple, en appelant la méthode EnterReadLock). Lorsqu'un thread demande un accès exclusif (par exemple, en appelant la méthode EnterWriteLock), les demandes des lecteurs suivantes sont bloquées jusqu'à ce que tous les lecteurs existants aient libéré le verrou et que le writer ait acquis et libéré le verrou.

ReaderWriterLockSlim possède l'affinité de thread.

Pour une vue d'ensemble conceptuelle, consultez Verrous de lecteur-writer.

Classe Semaphore

La classe Semaphore permet à un nombre spécifié de threads d'accéder à une ressource. Les autres threads qui demandent la ressource sont bloqués jusqu'à ce qu'un thread libère le sémaphore.

À l'instar de la classe Mutex, Semaphore dérive de WaitHandle. Et tout comme Mutex, Semaphore peut être local ou global. Il peut être utilisé au-delà des limites de domaine d'application.

Contrairement à Monitor, Mutex et ReaderWriterLock, Semaphore n'a pas d'affinité de thread. Cela signifie qu'il peut être utilisé dans les scénarios où un thread acquiert le sémaphore et un autre le libère.

Pour une vue d'ensemble conceptuelle, consultez Sémaphores.

Signalisation

Pour attendre un signal d'un autre thread, le plus simple consiste à appeler la méthode Join qui se bloque jusqu'à ce que l'autre thread achève son exécution. Join possède deux surcharges qui permettent au thread bloqué d'abandonner l'attente au terme d'un intervalle spécifié.

Les handles d'attente fournissent un ensemble beaucoup plus complet de fonctions d'attente et de signalisation.

Handles d'attente

Les handles d'attente dérivent de la classe WaitHandle qui, elle-même, dérive de MarshalByRefObject. Par conséquent, les handles d'attente peuvent être utilisés pour synchroniser les activités des threads au-delà des limites de domaine d'application.

Les threads se bloquent sur les handles d'attente en appelant la méthode d'instance WaitOne ou l'une des méthodes statiques WaitAll, WaitAny ou SignalAndWait. La façon dont ils sont libérés dépend de la méthode appelée, et du type de handles d'attente.

Pour une vue d'ensemble conceptuelle, consultez Handles d'attente.

Handles d'attente d'événement

Les handles d'attente d'événement incluent la classe EventWaitHandle et ses classes dérivées AutoResetEvent et ManualResetEvent. Les threads sont libérés d'un handle d'attente d'événement lorsque ce dernier en reçoit le signal par l'appel à sa méthode Set ou à l'aide de la méthode SignalAndWait.

Les handles d'attente d'événement se réinitialisent automatiquement, à l'instar d'un tourniquet qui permet le passage d'un seul thread à chaque signal reçu, ou ils doivent être réinitialisés manuellement, comme une barrière qui reste fermée jusqu'à la réception d'un signal d'ouverture et reste ouverte jusqu'à ce qu'on la ferme. Comme leurs noms l'indiquent, AutoResetEvent et ManualResetEvent représentent respectivement le premier et le second type de réinitialisation.

EventWaitHandle peut représenter les deux types d'événements et peut être local ou global. Les classes dérivées AutoResetEvent et ManualResetEvent sont toujours locales.

Les handles d'attente d'événement n'ont pas d'affinité de thread. N'importe quel thread peut envoyer un signal à un handle d'attente d'événement.

Pour une vue d'ensemble conceptuelle, consultez EventWaitHandle, AutoResetEvent et ManualResetEvent.

Classes Mutex et Semaphore

Dans la mesure où les classes Mutex et Semaphore dérivent de WaitHandle, elles peuvent être utilisées avec les méthodes statiques de WaitHandle. Par exemple, un thread peut utiliser la méthode WaitAll pour attendre que les trois conditions suivantes soient vraies : EventWaitHandle est signalé, Mutex est libéré et Semaphore est libéré. De la même façon, un thread peut utiliser la méthode WaitAny pour attendre que l'une de ces conditions soit vraie.

Pour Mutex ou Semaphore, recevoir un signal signifie être libéré. Si l'un des deux types est utilisé comme premier argument de la méthode SignalAndWait, il est libéré. Dans le cas de Mutex, qui possède l'affinité de thread, une exception est levée si le thread appelant n'est pas propriétaire du mutex. Comme mentionné précédemment, les sémaphores n'ont pas d'affinité de thread.

Opérations verrouillées

Les opérations verrouillées sont de simples opérations atomiques exécutées dans un emplacement de mémoire par les méthodes statiques de la classe Interlocked. Ces opérations atomiques incluent l'addition, l'incrémentation et la décrémentation, l'échange et l'échange conditionnel en fonction d'une comparaison ainsi que des opérations de lecture pour des valeurs 64 bits sur les plateformes 32 bits.

Remarque :

L'atomicité n'est garantie que dans le cadre d'opérations individuelles ; lorsque plusieurs opérations doivent être exécutées en tant qu'unité, un mécanisme de synchronisation moins granulaire doit être utilisé.

Bien qu'aucune de ces opérations ne constitue un verrou ou un signal, elles peuvent être utilisées pour construire des verrous et des signaux. Étant natives au système d'exploitation Windows, les opérations verrouillées sont extrêmement rapides.

Elles peuvent être utilisées avec des garanties de mémoire volatile pour écrire des applications qui proposent un accès concurrentiel non bloquant puissant. Toutefois, elles exigent une programmation de bas niveau élaborée et dans la plupart des cas, les verrous simples constituent une solution mieux adaptée.

Pour une vue d'ensemble conceptuelle, consultez Opérations verrouillées.

Voir aussi

Concepts

Synchronisation des données pour le multithreading

Moniteurs

Mutex

Sémaphores

Handles d'attente

Opérations verrouillées

Verrous de lecteur-writer

Autres ressources

EventWaitHandle, AutoResetEvent et ManualResetEvent