Quelles sont les différences entre les différentes options de synchronisation des threads en C #?

164

Quelqu'un peut-il expliquer la différence entre:

  • verrouiller (un objet) {}
  • Utiliser Mutex
  • Utilisation du sémaphore
  • Utilisation du moniteur
  • Utilisation d'autres classes de synchronisation .Net

Je n'arrive tout simplement pas à comprendre. Il me semble que les deux premiers sont les mêmes?

user38834
la source
Ce lien m'a beaucoup aidé: albahari.com/threading
Raphael

Réponses:

135

Excellente question. J'ai peut-être tort .. Laissez-moi essayer .. Révision n ° 2 de ma réponse d'origine .. avec un peu plus de compréhension. Merci de m'avoir fait lire :)

verrouiller (obj)

  • est une construction CLR pour la synchronisation des threads (intra-objet?). Garantit qu'un seul thread peut prendre possession du verrou de l'objet et entrer dans le bloc de code verrouillé. Les autres threads doivent attendre que le propriétaire actuel abandonne le verrou en quittant le bloc de code. Il est également recommandé de verrouiller un objet membre privé de votre classe.

Moniteurs

  • lock (obj) est implémenté en interne à l'aide d'un moniteur. Vous devriez préférer lock (obj) car cela vous empêche de faire des gogo comme si vous oubliez la procédure de nettoyage. C'est la construction du moniteur à l'épreuve des idiots si vous voulez.
    L'utilisation de Monitor est généralement préférable aux mutex, car les moniteurs ont été conçus spécifiquement pour .NET Framework et utilisent donc mieux les ressources.

L'utilisation d'un verrou 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 qui ont l'un des deux états, signalé et non signalé, qui peuvent être utilisés pour activer et suspendre les threads. Mutex, les sémaphores sont des concepts au niveau du système d'exploitation. par exemple, avec un mutex nommé, vous pouvez synchroniser plusieurs ex (gérés) (en vous assurant qu'une seule instance de votre application est en cours d'exécution sur la machine.)

Mutex:

  • Contrairement aux moniteurs, cependant, un mutex peut être utilisé pour synchroniser les threads entre les processus. Lorsqu'il est utilisé pour la synchronisation inter-processus, un mutex est appelé un mutex nommé car il doit être utilisé dans une autre application et ne peut donc pas être partagé au moyen d'une variable globale ou statique. Il doit recevoir un nom pour que les deux applications puissent accéder au même objet mutex. En revanche, la classe Mutex est un wrapper pour une construction Win32. Bien qu'il soit plus puissant qu'un moniteur, un mutex nécessite des transitions d'interopérabilité qui sont plus coûteuses en calcul que celles requises par la classe Monitor.

Sémaphores (me fait mal au cerveau).

  • Utilisez la classe Semaphore pour contrôler l'accès à un pool de ressources. Les threads entrent dans le sémaphore en appelant la méthode WaitOne, qui est héritée de la classe WaitHandle, et libèrent le sémaphore en appelant la méthode Release. Le décompte sur un sémaphore est décrémenté chaque fois qu'un thread entre dans le sémaphore, et incrémenté lorsqu'un thread libère le sémaphore. Lorsque le nombre est égal à zéro, les demandes suivantes se bloquent jusqu'à ce que d'autres threads libèrent le sémaphore. Lorsque tous les threads ont libéré le sémaphore, le nombre est à la valeur maximale spécifiée lors de la création du sémaphore. Un thread peut entrer dans le sémaphore plusieurs fois .. La classe Semaphore n'applique pas l'identité de thread sur WaitOne ou Release .. la responsabilité des programmeurs de ne pas salir. Les sémaphores sont de deux types: les sémaphores locaux et nomméssémaphores système. Si vous créez un objet Semaphore à l'aide d'un constructeur qui accepte un nom, il est associé à un sémaphore du système d'exploitation de ce nom. Les sémaphores système nommés sont visibles dans tout le système d'exploitation et peuvent être utilisés pour synchroniser les activités des processus. Un sémaphore local n'existe que dans votre processus. Il peut être utilisé par n'importe quel thread de votre processus qui a une référence à l'objet Semaphore local. Chaque objet Semaphore est un sémaphore local distinct.

LA PAGE À LIRE - Synchronisation des threads (C #)

Gishu
la source
18
Vous prétendez que la Monitorcommunication n'autorise pas la communication est incorrecte; vous pouvez encore Pulseetc avec unMonitor
Marc Gravell
3
Consultez une description alternative de Sémaphores - stackoverflow.com/a/40473/968003 . Considérez les sémaphores comme des videurs dans une discothèque. Il y a un nombre dédié de personnes qui sont autorisées dans le club à la fois. Si le club est plein, personne n'est autorisé à entrer, mais dès qu'une personne quitte, une autre personne peut entrer.
Alex Klaus
31

Re "Utilisation d'autres classes de synchronisation .Net" - quelques-unes des autres que vous devriez connaître:

Il existe également plus de constructions de verrouillage (à faible surcharge) dans CCR / TPL (le CTP des extensions parallèles ) - mais IIRC, elles seront disponibles dans .NET 4.0

Marc Gravell
la source
Donc, si je veux une simple communication de signal (disons l'achèvement d'une opération asynchrone) - je devrais Monitor.Pulse? ou utiliser SemaphoreSlim ou TaskCompletionSource?
Vivek
Utilisez TaskCompletionSource pour une opération asynchrone. En gros, arrêtez de penser aux fils et commencez à penser aux tâches (unités de travail). Les threads sont un détail d'implémentation et ne sont pas pertinents. En renvoyant un TCS, vous pouvez renvoyer des résultats, des erreurs ou gérer l'annulation et il est facilement composable avec d'autres opérations asynchrones (telles que async await ou ContinueWith).
Simon Gillbee
14

Comme indiqué dans ECMA, et comme vous pouvez l'observer à partir des méthodes Reflected, l'instruction lock est fondamentalement équivalente à

object obj = x;
System.Threading.Monitor.Enter(obj);
try {
   
}
finally {
   System.Threading.Monitor.Exit(obj);
}

À partir de l'exemple susmentionné, nous voyons que les moniteurs peuvent se verrouiller sur des objets.

Les mutex sont utiles lorsque vous avez besoin d'une synchronisation interprocessus car ils peuvent se verrouiller sur un identificateur de chaîne. Le même identifiant de chaîne peut être utilisé par différents processus pour acquérir le verrou.

Les sémaphores sont comme les Mutex sous stéroïdes, ils permettent un accès simultané en fournissant un nombre maximum d'accès simultanés ». Une fois la limite atteinte, le sémaphore commence à bloquer tout accès ultérieur à la ressource jusqu'à ce que l'un des appelants libère le sémaphore.

arul
la source
5
Ce sucre syntaxique a été légèrement modifié en C # 4 Consultez blogs.msdn.com/ericlippert/archive/2009/03/06/…
Peter Gfader
14

J'ai fait les cours et le support CLR pour le threading dans DotGNU et j'ai quelques réflexions ...

Sauf si vous avez besoin de verrous de processus croisés, vous devez toujours éviter d'utiliser Mutex et sémaphores. Ces classes dans .NET sont des enveloppes autour du mutex et des sémaphores Win32 et sont plutôt lourdes (elles nécessitent un changement de contexte dans le noyau, ce qui coûte cher - surtout si votre verrou n'est pas en conflit).

Comme d'autres sont mentionnés, l'instruction C # lock est la magie du compilateur pour Monitor.Enter et Monitor.Exit (existant dans un try / finally).

Les moniteurs ont un mécanisme de signal / attente simple mais puissant que les Mutex n'ont pas via les méthodes Monitor.Pulse / Monitor.Wait. L'équivalent Win32 serait des objets événement via CreateEvent qui existent également dans .NET en tant que WaitHandles. Le modèle Pulse / Wait est similaire à pthread_signal et pthread_wait d'Unix mais est plus rapide car il peut s'agir d'opérations entièrement en mode utilisateur dans le cas non contesté.

Monitor.Pulse / Wait est simple à utiliser. Dans un thread, nous verrouillons un objet, vérifions un drapeau / état / propriété et si ce n'est pas ce que nous attendons, appelons Monitor.Wait qui relâchera le verrou et attendra qu'une impulsion soit envoyée. Lorsque l'attente revient, nous bouclons et vérifions à nouveau l'indicateur / l'état / la propriété. Dans l'autre thread, nous verrouillons l'objet chaque fois que nous modifions l'indicateur / état / propriété, puis appelons PulseAll pour réveiller les threads d'écoute.

Souvent, nous voulons que nos classes soient thread-safe, donc nous mettons des verrous dans notre code. Cependant, il arrive souvent que notre classe ne soit utilisée que par un seul thread. Cela signifie que les verrous ralentissent inutilement notre code ... c'est là que des optimisations intelligentes du CLR peuvent aider à améliorer les performances.

Je ne suis pas sûr de l'implémentation des verrous par Microsoft, mais dans DotGNU et Mono, un indicateur d'état de verrouillage est stocké dans l'en-tête de chaque objet. Chaque objet dans .NET (et Java) peut devenir un verrou, chaque objet doit donc le prendre en charge dans son en-tête. Dans l'implémentation DotGNU, il existe un indicateur qui vous permet d'utiliser une table de hachage globale pour chaque objet utilisé comme verrou - cela a l'avantage d'éliminer une surcharge de 4 octets pour chaque objet. Ce n'est pas génial pour la mémoire (en particulier pour les systèmes embarqués qui ne sont pas fortement threadés) mais a un impact négatif sur les performances.

Mono et DotGNU utilisent efficacement des mutex pour effectuer le verrouillage / attente, mais utilisent des opérations de comparaison et d'échange de style spinlock pour éliminer le besoin d'effectuer réellement des verrous durs sauf si vraiment nécessaire:

Vous pouvez voir un exemple de la façon dont les moniteurs peuvent être mis en œuvre ici:

http://cvs.savannah.gnu.org/viewvc/dotgnu-pnet/pnet/engine/lib_monitor.c?revision=1.7&view=markup

tumtumtum
la source
9

Une mise en garde supplémentaire pour le verrouillage sur tout Mutex partagé que vous avez identifié avec un ID de chaîne est qu'il sera par défaut un mutex "Local \" et ne sera pas partagé entre les sessions dans un environnement de serveur de terminaux.

Préfixez votre identifiant de chaîne avec "Global \" pour vous assurer que l'accès aux ressources système partagées est correctement contrôlé. J'étais juste en train de rencontrer un tas de problèmes de synchronisation des communications avec un service fonctionnant sous le compte SYSTEM avant de m'en rendre compte.

nvuono
la source
5

J'essaierais d'éviter "lock ()", "Mutex" et "Monitor" si vous le pouvez ...

Découvrez le nouvel espace de noms System.Collections.Concurrent dans .NET 4
Il a quelques classes de collection thread-safe

http://msdn.microsoft.com/en-us/library/system.collections.concurrent.aspx

ConcurrentDictionary rochers! plus de verrouillage manuel pour moi!

Peter Gfader
la source
2
Évitez le verrouillage mais utilisez Monitor? Pourquoi?
mafu
@mafutrct Parce que vous devez vous occuper de la synchronisation vous-même.
Peter Gfader
Oh, maintenant je comprends, vous vouliez éviter TOUTES les trois idées mentionnées. Cela sonnait comme si vous utilisiez Monitor mais pas lock / Mutex.
mafu
N'utilisez jamais System.Collections.Concurrent. Ils sont une source principale de conditions de concurrence et bloquent également le fil des appelants.
Alexander Danilov le
-2

Dans la plupart des cas, vous ne devez pas utiliser de verrous (= moniteurs) ou de mutex / sémaphores. Ils bloquent tous le fil actuel.

Et vous ne devriez certainement pas utiliser deSystem.Collections.Concurrent classes - elles sont la principale source de conditions de concurrence car elles ne prennent pas en charge les transactions entre plusieurs collections et bloquent également le thread actuel.

Étonnamment .NET n'a pas de mécanismes efficaces de synchronisation.

J'ai implémenté la file d' attente série de GCD ( Objc/Swiftmonde) sur C # - un outil de synchronisation très léger, non bloquant qui utilise un pool de threads, avec des tests.

C'est le meilleur moyen de synchroniser quoi que ce soit dans la plupart des cas - de l'accès à la base de données (bonjour sqlite) à la logique métier.

Alexandre Danilov
la source