Partager via


Vue d'ensemble des primitives de synchronisation

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.

Cette vue d'ensemble contient les sections suivantes :

  • Verrouillage

  • Signalisation

  • Types de synchronisation légers

  • SpinWait

  • Opérations verrouillées

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 interblocages 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 interblocages 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.

Classe SpinLock

À compter du .NET Framework version 4, vous pouvez utiliser la classe SpinLock lorsque la charge mémoire requise par Monitor dégrade les performances. Lorsque SpinLock rencontre une section critique verrouillée, il tourne simplement dans une boucle jusqu'à ce que le verrou devienne disponible. Si le verrou est maintenu pendant une durée très courte, la rotation peut fournir de meilleures performances que le blocage. Toutefois, si le verrou est maintenu pendant plus de quelques dizaines de cycles, SpinLock fonctionne aussi bien que Monitor, mais utilise plus de cycles microprocesseurs et peut donc dégrader les performances d'autres threads ou processus.

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 Semaphore et SemaphoreSlim.

System.Threading.SemaphoreSlim est un sémaphore léger pour la synchronisation dans une limite de processus unique.

Retour au début

Signalisation

La méthode la plus simple pour attendre un signal d'un autre thread consiste à appeler la méthode Join, qui se bloque jusqu'à ce que l'autre thread soit terminé. Join a deux surcharges qui autorisent le thread bloqué à sortir de l'attente après qu'un intervalle spécifié s'est écoulé.

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. System.Threading.ManualResetEventSlim est un événement léger pour la synchronisation dans une limite de processus unique.

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, CountdownEvent 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.

Cloisonnement

La classe Barrier offre un moyen de synchroniser plusieurs threads cycliquement afin qu'ils se bloquent tous au même point et attendent que tous les autres threads soient terminés. Le cloisonnement est utile lorsqu'un ou plusieurs threads requièrent les résultats d'un autre thread avant de passer à la phase suivante d'un algorithme. Pour plus d'informations, consultez Cloisonnement (.NET Framework).

Retour au début

Types de synchronisation légers

À partir du .NET Framework 4, vous pouvez utiliser des primitives de synchronisation qui offrent des performances rapides en évitant une dépendance complexe sur les objets de noyau Win32, tels que les handles d'attente, à chaque fois que possible. En général, vous devez utiliser ces types lorsque les temps d'attente sont courts et uniquement lorsque les types de synchronisation d'origine ont été essayés et ont donné des résultats peu satisfaisants. Les types légers ne peuvent pas être utilisés dans les scénarios qui requièrent une communication interprocessus.

Retour au début

SpinWait

À compter du .NET Framework 4, vous pouvez utiliser la structure System.Threading.SpinWait lorsqu'un thread doit attendre le signalement d'un événement ou la satisfaction d'une condition, mais lorsque le temps d'attente réel est supposé être inférieur à la latence requise avec l'utilisation d'un handle d'attente ou le blocage de toute autre façon du thread actuel. Grâce à SpinWait, vous pouvez spécifier une courte période de rotation d'attente, puis générer (par exemple, attente ou veille) uniquement si la condition n'a pas été satisfaite pendant la durée spécifiée.

Retour au début

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.

RemarqueRemarque

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 donc, 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.

Retour au début

Voir aussi

Concepts

Synchronisation des données pour le multithreading

Moniteurs

Mutex

Semaphore et SemaphoreSlim

Handles d'attente

Opérations verrouillées

Verrous de lecteur-writer

Cloisonnement (.NET Framework)

SpinLock

Autres ressources

EventWaitHandle, AutoResetEvent, CountdownEvent et ManualResetEvent

SpinWait