2009-07-12 10 views
12

I mis en œuvre le fil de traitement de fond suivant, où Jobs est un Queue<T>:ManualResetEvent contre Thread.Sleep

static void WorkThread() 
{ 
    while (working) 
    { 
     var job; 

     lock (Jobs) 
     { 
      if (Jobs.Count > 0) 
       job = Jobs.Dequeue(); 
     } 

     if (job == null) 
     { 
      Thread.Sleep(1); 
     } 
     else 
     { 
      // [snip]: Process job. 
     } 
    } 
} 

Ce produit un retard notable entre le moment où les emplois étaient entrés et quand ils ont été effectivement commencé à être exécuter (des lots d'emplois sont entrés en même temps, et chaque travail est seulement [relativement] petit.) Le retard n'était pas énorme, mais j'ai pensé au problème et j'ai apporté le changement suivant:

static ManualResetEvent _workerWait = new ManualResetEvent(false); 
// ... 
    if (job == null) 
    { 
     lock (_workerWait) 
     { 
      _workerWait.Reset(); 
     } 
     _workerWait.WaitOne(); 
    } 

Où le fil l'ajout de travaux verrouille désormais _workerWait et appelle _workerWait.Set() lorsqu'il a terminé d'ajouter des tâches. Cette solution (apparemment) démarre instantanément les tâches de traitement, et le retard est complètement disparu.

Ma question est en partie "Pourquoi cela arrive-t-il?", Sachant que Thread.Sleep(int) peut très bien dormir plus longtemps que vous ne le spécifiez, et en partie "Comment le ManualResetEvent atteint-il ce niveau de performance?".

EDIT: Depuis que quelqu'un a demandé à propos de la fonction qui met en file d'attente les éléments, le voici, avec le système complet tel qu'il se présente pour l'instant.

public void RunTriggers(string data) 
{ 
    lock (this.SyncRoot) 
    { 
     this.Triggers.Sort((a, b) => { return a.Priority - b.Priority; }); 

     foreach (Trigger trigger in this.Triggers) 
     { 
      lock (Jobs) 
      { 
       Jobs.Enqueue(new TriggerData(this, trigger, data)); 
       _workerWait.Set(); 
      } 
     } 
    } 
} 

static private ManualResetEvent _workerWait = new ManualResetEvent(false); 
static void WorkThread() 
{ 
    while (working) 
    { 
     TriggerData job = null; 

     lock (Jobs) 
     { 
      if (Jobs.Count > 0) 
       job = Jobs.Dequeue(); 

      if (job == null) 
      { 
       _workerWait.Reset(); 
      } 
     } 

     if (job == null) 
      _workerWait.WaitOne(); 
     else 
     { 
      try 
      { 
       foreach (Match m in job.Trigger.Regex.Matches(job.Data)) 
        job.Trigger.Value.Action(job.World, m); 
      } 
      catch (Exception ex) 
      { 
       job.World.SendLineToClient("\r\n\x1B[32m -- {0} in trigger ({1}): {2}\x1B[m", 
        ex.GetType().ToString(), job.Trigger.Name, ex.Message); 
      } 
     } 
    } 
} 

Répondre

15

Les événements sont des primitives de noyau fournies par l'OS/Kernel qui est conçu juste pour ce genre de choses. Le noyau fournit une limite sur laquelle vous pouvez garantir les opérations atomiques, ce qui est important pour la synchronisation (une certaine atomicité peut également être effectuée dans l'espace utilisateur avec le support matériel). En bref, lorsqu'un thread attend un événement, il est placé sur une liste d'attente pour cet événement et marqué comme non exécutable. Lorsque l'événement est signalé, le noyau réveille ceux de la liste d'attente et les marque comme exécutables et ils peuvent continuer à fonctionner. C'est naturellement un énorme avantage qu'un thread peut se réveiller immédiatement lorsque l'événement est signalé, par opposition à une longue période de sommeil et revérifier la condition de temps en temps.

Même une milliseconde est vraiment très longue, vous auriez pu traiter des milliers d'événements pendant cette période. En outre, la résolution temporelle est traditionnellement de 10 ms, donc dormir moins de 10 ms entraîne généralement un sommeil de 10 ms de toute façon. Avec un événement, un thread peut être réveillé et programmé immédiatement

+2

Information la plus récente: la résolution minimale de 10ms est une XP et une chose antérieure car le système d'exploitation utilisait des incréments statiques de 10ms pour la programmation. Je pense que Vista, et je sais que Win7 le fait, utilise une tranche de temps dynamique "tickless". Avec Win7, je peux démarrer une minuterie haute résolution, émettre un sleep (1), et le timing est extrêmement proche de 1ms, parfois inférieur à. – Bengie

10

verrouillage d'abord sur _workerWait est inutile, un objet de l'événement est un système (noyau) conçu pour la signalisation entre des fils (et très utilisé dans l'API Win32 pour les opérations asynchrones). Par conséquent, il est relativement sûr pour plusieurs threads de le définir ou de le réinitialiser sans synchronisation supplémentaire. En ce qui concerne votre question principale, vous devez également voir la logique de placement des éléments dans la file d'attente, ainsi que des informations sur le travail effectué pour chaque travail (le thread de travail passe-t-il plus de temps à travailler ou à attendre?). travail).

La meilleure solution serait probablement d'utiliser une instance d'objet pour verrouiller et utiliser Monitor.Pulse et Monitor.Wait comme variable de condition. Edit: Avec une vue du code à mettre en file d'attente, il semble que la réponse #1116297 a raison: un délai de 1 ms est trop long à attendre, étant donné que de nombreux éléments de travail seront extrêmement rapides à traiter.

L'approche d'avoir un mécanisme pour réveiller le thread de travail est correcte (car il n'y a pas de file d'attente concurrente .NET avec une opération de dequeue bloquante). Cependant plutôt que d'utiliser un événement, une variable de condition va être un peu plus efficace (comme dans les cas non-soutenu, il ne nécessite pas une transition du noyau):

object sync = new Object(); 
var queue = new Queue<TriggerData>(); 

public void EnqueueTriggers(IEnumerable<TriggerData> triggers) { 
    lock (sync) { 
    foreach (var t in triggers) { 
     queue.Enqueue(t); 
    } 
    Monitor.Pulse(sync); // Use PulseAll if there are multiple worker threads 
    } 
} 

void WorkerThread() { 
    while (!exit) { 
    TriggerData job = DequeueTrigger(); 
    // Do work 
    } 
} 

private TriggerData DequeueTrigger() { 
    lock (sync) { 
    if (queue.Count > 0) { 
     return queue.Dequeue(); 
    } 
    while (queue.Count == 0) { 
     Monitor.Wait(sync); 
    } 
    return queue.Dequeue(); 
    } 
} 

Monitor.Wait libérera le verrou sur la paramètre, attendez Pulse() ou PulseAll() est appelé contre le verrou, puis entrez à nouveau le verrou et retour. Besoin de revérifier la condition d'attente car un autre thread aurait pu lire l'élément hors de la file d'attente.

+0

La majorité des tâches correspondront à un Regex (précompilé) et quitteront (car la correspondance a échoué). Le nombre dépend du nombre d'utilisateurs et de la quantité de données reçues par l'application (c'est une application réseau). Il est très possible qu'il atteigne plusieurs centaines de secondes à la charge maximale, peut-être jusqu'à un millier. Je ne savais pas si quelqu'un serait intéressé par les éléments de mise en file d'attente de code, mais je l'édite maintenant, puisque vous l'avez demandé si gentiment :) –

+0

Je pensais avoir lu quelque part que Monitor était le support derrière la serrure() {} construction? Comment est-ce que vous pouvez utiliser lock() et Monitor sur le même objet de synchronisation comme ça? –

+0

Oh, attends, je viens de lire et de comprendre le dernier paragraphe là. –