C #: Monitor – Attente, impulsion, impulsionAll

J’ai du mal à comprendre Wait() , Pulse() , PulseAll() . Est-ce qu’ils éviteront tous l’impasse? J’apprécierais si vous expliquez comment les utiliser?

Version courte:

 lock(obj) {...} 

est raccourci pour Monitor.Enter / Monitor.Exit (avec gestion des exceptions, etc.). Si personne d’autre n’a le verrou, vous pouvez l’obtenir (et exécuter votre code) – sinon votre thread est bloqué jusqu’à ce que le verrou soit acquis (par un autre thread le relâchant).

Le blocage survient généralement lorsque l’un des deux threads verrouille des objects dans des ordres différents:

 thread 1: lock(objA) { lock (objB) { ... } } thread 2: lock(objB) { lock (objA) { ... } } 

(ici, s’ils acquièrent chacun le premier verrou, aucun ne peut jamais obtenir le second, car aucun thread ne peut sortir pour libérer son verrou)

Ce scénario peut être minimisé en verrouillant toujours dans le même ordre; et vous pouvez récupérer (dans une certaine mesure) en utilisant Monitor.TryEnter (au lieu de Monitor.Enter / lock ) et en spécifiant un délai d’attente.

ou B: vous pouvez vous bloquer avec des choses comme les winforms lors du changement de thread tout en maintenant un verrou:

 lock(obj) { // on worker this.Invoke((MethodInvoker) delegate { // switch to UI lock(obj) { // oopsiee! ... } }); } 

L’impasse semble évidente ci-dessus, mais ce n’est pas si évident quand vous avez un code spaghetti; Réponses possibles: ne changez pas de thread tout en maintenant les verrous, ou utilisez BeginInvoke pour pouvoir au moins quitter le verrou (en laissant jouer l’interface utilisateur).


Wait / Pulse / PulseAll sont différents; ils sont pour la signalisation. Je l’utilise dans cette réponse pour signaler que:

  • Dequeue : si vous essayez de retirer les données lorsque la queue est vide, il attend qu’un autre thread ajoute des données, ce qui réveille le thread bloqué.
  • Enqueue : si vous essayez de mettre en queue des données lorsque la queue est pleine, il attend qu’un autre thread supprime les données, ce qui réveille le thread bloqué.

Pulse ne réveille qu’un seul thread – mais je ne suis pas assez intelligent pour prouver que le prochain thread est toujours celui que je veux, alors j’ai tendance à utiliser PulseAll , et à simplement vérifier les conditions avant de continuer; par exemple:

  while (queue.Count >= maxSize) { Monitor.Wait(queue); } 

Avec cette approche, je peux append en toute sécurité d’autres significations de Pulse , sans que mon code existant suppose que “je me suis réveillé, donc il y a des données” – ce qui est pratique quand (dans le même exemple) j’ai ensuite ajouté une méthode Close() .

Recette simple pour utiliser Monitor.Wait et Monitor.Pulse. Il se compose d’un travailleur, d’un patron et d’un téléphone avec lequel ils communiquent:

 object phone = new object(); 

Un fil “travailleur”:

 lock(phone) // Sort of "Turn the phone on while at work" { while(true) { Monitor.Wait(phone); // Wait for a signal from the boss DoWork(); Monitor.PulseAll(phone); // Signal boss we are done } } 

Un fil “Boss”:

 PrepareWork(); lock(phone) // Grab the phone when I have something ready for the worker { Monitor.PulseAll(phone); // Signal worker there is work to do Monitor.Wait(phone); // Wait for the work to be done } 

Des exemples plus complexes suivent …

Un “travailleur avec autre chose à faire”:

 lock(phone) { while(true) { if(Monitor.Wait(phone,1000)) // Wait for one second at most { DoWork(); Monitor.PulseAll(phone); // Signal boss we are done } else DoSomethingElse(); } } 

Un “Boss Impatient”:

 PrepareWork(); lock(phone) { Monitor.PulseAll(phone); // Signal worker there is work to do if(Monitor.Wait(phone,1000)) // Wait for one second at most Console.Writeline("Good work!"); } 

Non, ils ne vous protègent pas des impasses. Ce sont des outils plus flexibles pour la synchronisation des threads. Voici une très bonne explication sur la façon de les utiliser et un schéma d’utilisation très important – sans ce modèle, vous allez tout casser: http://www.albahari.com/threading/part4.aspx

Lisez l’ article sur le filetage en plusieurs parties de Jon Skeet .

C’est vraiment bien. Ceux que vous mentionnez sont à environ un tiers du chemin.

Ce sont des outils de synchronisation et de signalisation entre les threads. En tant que tels, ils ne font rien pour empêcher les blocages, mais s’ils sont utilisés correctement, ils peuvent être utilisés pour synchroniser et communiquer entre les threads.

Malheureusement, la plupart du travail nécessaire pour écrire du code multithread correct est actuellement la responsabilité des développeurs en C # (et dans de nombreuses autres langues). Jetez un coup d’œil à la façon dont F #, Haskell et Clojure traitent cela pour une approche totalement différente.

Malheureusement, aucun de Wait (), Pulse () ou PulseAll () ne possède la propriété magique que vous souhaitez – ce qui signifie qu’en utilisant cette API, vous éviterez automatiquement les interblocages.

Considérez le code suivant

 object incomingMessages = new object(); //signal object LoopOnMessages() { lock(incomingMessages) { Monitor.Wait(incomingMessages); } if (canGrabMessage()) handleMessage(); // loop } ReceiveMessagesAndSignalWaiters() { awaitMessages(); copyMessagesToReadyArea(); lock(incomingMessages) { Monitor.PulseAll(incomingMessages); //or Monitor.Pulse } awaitReadyAreaHasFreeSpace(); } 

Ce code sera bloqué! Peut-être pas aujourd’hui, peut-être pas demain. Très probablement lorsque votre code est mis sous tension parce que soudainement il est devenu populaire ou important, et vous êtes appelé à résoudre un problème urgent.

Pourquoi?

Finalement, ce qui suit se produira:

  1. Tous les threads consommateurs travaillent
  2. Les messages arrivent, la zone prête ne peut plus contenir de messages et PulseAll () est appelée.
  3. Aucun consommateur n’est réveillé, car aucun n’attend
  4. Tous les threads consommateurs appellent Wait () [DEADLOCK]

Cet exemple particulier suppose que le thread producteur n’appellera plus jamais PulseAll () car il n’a plus d’espace pour y placer des messages. Mais il existe de nombreuses variantes de ce code. Les gens essaieront de le rendre plus robuste en changeant une ligne telle que Monitor.Wait(); dans

 if (!canGrabMessage()) Monitor.Wait(incomingMessages); 

Malheureusement, cela ne suffit toujours pas à le réparer. Pour résoudre ce problème, vous devez également modifier l’étendue de locking où Monitor.PulseAll() est appelé:

 LoopOnMessages() { lock(incomingMessages) { if (!canGrabMessage()) Monitor.Wait(incomingMessages); } if (canGrabMessage()) handleMessage(); // loop } ReceiveMessagesAndSignalWaiters() { awaitMessagesArrive(); lock(incomingMessages) { copyMessagesToReadyArea(); Monitor.PulseAll(incomingMessages); //or Monitor.Pulse } awaitReadyAreaHasFreeSpace(); } 

Le point clé est que dans le code fixe, les verrous limitent les séquences d’événements possibles:

  1. Un fil de consommation fait son travail et ses boucles

  2. Ce fil acquiert le verrou

    Et grâce au locking, il est maintenant vrai que:

  3. une. Les messages ne sont pas encore arrivés dans la zone de préparation et libèrent le verrou en appelant Wait () AVANT que le thread destinataire du message ne puisse acquérir le verrou et copier davantage de messages dans la zone de préparation, ou

    b. Les messages sont déjà arrivés dans la zone prête et il reçoit les messages INSTEAD OF appelant Wait (). (Et pendant qu’il prend cette décision, il est impossible pour le thread du récepteur de messages d’acquérir le verrou et de copier davantage de messages dans la zone de préparation.)

En conséquence, le problème du code d’origine ne se produit plus jamais: 3. Lorsque PulseEvent () est appelé Aucun consommateur n’est réveillé, car aucun n’attend

Maintenant, observez que dans ce code, vous devez obtenir exactement la scope de locking . (Si, en effet j’ai bien compris!)

Et aussi, puisque vous devez utiliser le lock (ou Monitor.Enter() etc.) pour utiliser Monitor.PulseAll() ou Monitor.Wait() de manière sans blocage, vous devez toujours vous inquiéter de la possibilité d’ autres des impasses qui se produisent à cause de ce locking.

Bottom line: ces API sont également faciles à bousiller et avec une impasse, c’est-à-dire très dangereux

Quelque chose que ce total m’a lancé, c’est que Pulse donne un “heads-up” à un thread dans un Wait . Le thread en attente ne continuera pas tant que le thread qui a effectué l’ Pulse n’a pas verrouillé le thread et que le thread en attente ne l’a pas réussi.

 lock(phone) // Grab the phone { Monitor.PulseAll(phone); // Signal worker Monitor.Wait(phone); // ****** The lock on phone has been given up! ****** } 

ou

 lock(phone) // Grab the phone when I have something ready for the worker { Monitor.PulseAll(phone); // Signal worker there is work to do DoMoreWork(); } // ****** The lock on phone has been given up! ****** 

Dans les deux cas, ce n’est que lorsque “le verrou du téléphone a été abandonné” qu’un autre thread peut l’obtenir.

Il se peut que d’autres threads attendent ce verrou de Monitor.Wait(phone) ou de lock(phone) . Seul celui qui gagne le verrou pourra continuer.