2009-10-13 7 views

Répondre

48

Version courte:

lock(obj) {...} 

est sténographie 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).

Deadlock se produit généralement lorsque A: deux fils verrouiller les choses dans un ordre différent:

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

(ici, si chacun d'acquérir la première écluse, ne peut jamais obtenir le second, car aucun thread peut quitter pour libérer son verrou)

Ce scénario peut être réduit 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'expiration.

ou B: vous pouvez vous bloquer avec des choses comme WinForms lorsque le fil de commutation 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 il est pas évident quand vous avez du code spaghetti; réponses possibles: ne passez pas les threads en maintenant des verrous, ou utilisez BeginInvoke pour pouvoir au moins quitter le verrou (laisser jouer l'interface utilisateur).


Wait/Pulse/PulseAll sont différents; ils sont pour la signalisation.J'utilise cette in this answer pour signaler afin que:

  • Dequeue: si vous essayez de données dequeue lorsque la file d'attente est vide, il attend un autre thread pour ajouter des données, qui se réveille le fil bloqué
  • Enqueue: si vous essayez de données enqueue lorsque la file d'attente est pleine, il attend un autre thread pour supprimer les données, ce qui réveille le thread bloqué

Pulse ne se réveille jusqu'à un fil - mais je ne suis pas assez intelligent pour prouver que le prochain fil je s toujours celui que je veux, donc j'ai tendance à utiliser PulseAll, et simplement re-vérifier les conditions avant de continuer; à titre d'exemple:

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

Avec cette approche, je peux en toute sécurité ajouter d'autres significations de Pulse, sans mon code existant en supposant que « je me suis réveillé, donc il y a des données » - ce qui est pratique (dans le même exemple) Plus tard, j'ai dû ajouter une méthode Close().

1

Lecture Jon Skeet's multi-part threading article.

C'est vraiment bon. Ceux que vous mentionnez sont à peu près au tiers.

+7

il ne mentionne même pas Pulse ou attendre! Lien vers un article de John Skeet ne fait pas automatiquement une bonne réponse ... –

+2

Oh vraiment? Qu'est-ce que c'est alors? 'http: // www.yoda.arachsys.com/csharp/threads/deadlocks.shtml' – Geo

+0

@Geo: oui, celui-ci s'adapte mieux;) –

7

Non, ils ne vous protègent pas des blocages. Ce ne sont que des outils plus flexibles pour la synchronisation des threads. Voici une très bonne explication comment les utiliser et très important modèle d'utilisation - sans ce modèle, vous casserez toutes les choses: http://www.albahari.com/threading/part4.aspx

+1

POUR NOVICES HAUTEMENT RECOMMANDER la page albahari est parfait! Parcourt le filetage et la synchronisation pas à pas avec des exemples clairs. – DanG

1

Ce sont des outils pour la synchronisation et la 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 de nombreuses autres langues). Jetez un oeil à la façon dont F #, Haskell et Clojure gère cela pour une approche entièrement différente.

37

Recette simple pour l'utilisation de Monitor.Wait et Monitor.Pulse. Il se compose d'un travailleur, un patron et un téléphone qu'ils utilisent pour communiquer:

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 "Impatient Boss":

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!"); 
} 
+0

Je ne comprends pas. Comment est-il possible que Boss et Worker soient en mode «téléphone» en même temps? – Marshall

+1

@Marshall Monitor.Wait libère le verrou «téléphone» à la ligne suivante (vraisemblablement au patron). –

+0

@DennisGorelik ahh, je vois. J'ai développé votre point [ci-dessous] (http://stackoverflow.com/a/42581381/1282864) – jdpilgrim

1

Malheureusement, aucune de Wait(), Pulse() ou PulseAll() ont la propriété magique qui vous Souhaiter - qui est que par en utilisant cette API vous éviterez automatiquement les interblocages.

Consultez 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 impasse! Peut-être pas aujourd'hui, peut-être pas demain. Très probablement lorsque votre code est soumis à un stress parce que soudainement il est devenu populaire ou important, et vous êtes appelé à résoudre un problème urgent.

Pourquoi?

Finalement ce qui se passera:

  1. Tous les sujets de consommation font un travail
  2. Les messages arrivent, la zone prêt ne peut pas contenir plus de messages et PulseAll() est appelée.
  3. Aucun consommateur se réveiller, parce qu'aucun attendent
  4. Tous les sujets de consommation appel en attente() [BLOCAGE]

Cet exemple particulier suppose que le fil de producteur ne va jamais appeler PulseAll() à nouveau parce qu'il n'a pas plus d'espace pour mettre des messages. Mais il y a beaucoup, beaucoup de variations brisées sur ce code possible. Les gens vont essayer de le rendre plus robuste en changeant une ligne comme faisant Monitor.Wait(); dans

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

Malheureusement, cela ne suffit pas encore pour le fixer. Pour résoudre ce problème, vous aussi besoin de changer la portée de verrouillage 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:

  1. Un fils de consommation fait son travail et boucles

  2. Ce fil acquiert le verrou

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

  3. a. Messages ont pas encore est arrivé dans la zone de préparation, et il libère le verrou en appelant Wait() avant que le fil du récepteur de message peut acquérir le verrou et copier plus 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 au lieu d'appeler Wait(). (Et pendant qu'il prend cette décision, il est impossible pour le fil de récepteur de message par exemple acquérir le verrou et copier plus de messages dans la zone de préparation.)

En conséquence, le problème du code d'origine maintenant ne se produit jamais : 3. Lorsque PulseEvent() est appelée Aucun consommateur se réveiller, parce qu'aucun attendent

maintenant observer que, dans ce code, vous devez obtenir le champ de verrouillage droit exactement. (Si, en effet, je l'ai eu droit!)

Et aussi, puisque vous devez utiliser le lock (ou Monitor.Enter() etc.) afin d'utiliser Monitor.PulseAll() ou Monitor.Wait() d'une manière sans blocage, vous avez encore à vous soucier de la possibilité de autres les blocages qui se produisent à cause de ce blocage.

Bottom line: ces API sont également faciles à vis et une impasse avec, par exemple tout à fait dangereux

0

Quelque chose que le total m'a jeté ici est que Pulse donne juste un « heads up » à un fil dans un Wait .Le thread Attente ne continuera pas jusqu'à ce que le thread qui a fait le Pulseabandonne le verrou et le thread en attente l'emporte avec succès.

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, il n'est pas jusqu'à ce que « le verrou sur le téléphone a été abandonné » qu'un autre thread peut obtenir.

Il peut y avoir d'autres threads en attente de ce verrou de Monitor.Wait(phone) ou lock(phone). Seul celui qui remportera le verrou pourra continuer.

Questions connexes