2009-07-12 3 views
12

I implementata in fondo thread di elaborazione, dove Jobs è un Queue<T>:ManualResetEvent vs 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. 
     } 
    } 
} 

Questo ha prodotto un ritardo notevole tra quando i processi venivano inseriti e quando sono stati effettivamente cominciano ad essere run (batch i lavori sono inseriti in una sola volta, e ogni lavoro è unico [relativamente] piccolo.) il ritardo non è stato un grande affare, ma ho avuto modo di pensare al problema, e ha fatto la seguente modifica:

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

Dove il thread l'aggiunta di lavori ora blocca _workerWait e chiama _workerWait.Set() quando ha terminato di aggiungere lavori. Questa soluzione (apparentemente) inizia immediatamente a elaborare i lavori, e il ritardo è scomparso del tutto.

la mia domanda è in parte "Perché accade questo?", Concesso che Thread.Sleep(int) può benissimo dormire per più di si specifica, e in parte "Come fa il ManualResetEvent raggiungere questo livello di prestazioni?".

EDIT: Poiché qualcuno ha chiesto circa la funzione che è in coda articoli, eccola, insieme con l'intero sistema così com'è al momento.

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); 
      } 
     } 
    } 
} 

risposta

15

Gli eventi sono primitive del kernel fornite dal sistema operativo/kernel progettato per questo tipo di cose. Il kernel fornisce un limite in base al quale è possibile garantire operazioni atomiche che sono importanti per la sincronizzazione (alcune atomicità possono essere eseguite anche nello spazio utente con supporto hardware).

In breve, quando un thread attende un evento viene inserito in una lista di attesa per quell'evento e contrassegnato come non eseguibile. Quando viene segnalato l'evento, il kernel riattiva quelli in lista d'attesa e li contrassegna come eseguibili e possono continuare a funzionare. È naturalmente un enorme vantaggio che un thread può svegliarsi immediatamente quando viene segnalato l'evento, a dormire a lungo e ricontrollare la condizione di tanto in tanto.

Anche un millisecondo è un tempo davvero molto lungo, si potrebbe aver elaborato migliaia di eventi in quel momento. Anche la risoluzione temporale è tradizionalmente 10 ms, quindi dormire meno di 10 ms di solito si traduce comunque in un sonno di 10 ms. Con un evento, una discussione può essere svegliata e pianificata immediatamente

+2

Informazioni più recenti: la risoluzione minima di 10 ms è un XP e prima cosa il sistema operativo utilizzava incrementi statici di 10 ms per la pianificazione. Penso che Vista, e so che Win7 lo fa, usa una fetta di tempo dinamica "senza tick". Con Win7, posso avviare un timer ad alta risoluzione, emettere un sonno (1) e il tempo è estremamente vicino a 1 ms, a volte inferiore a. – Bengie

10

Prima bloccaggio _workerWait è inutile, un evento è un oggetto di sistema (kernel) progettata per la segnalazione tra i thread (e ampiamente utilizzati nella API Win32 per operazioni asincrone). Pertanto è abbastanza sicuro che più thread possano essere impostati o ripristinati senza ulteriore sincronizzazione.

Per quanto riguarda la domanda principale, è necessario vedere la logica per posizionare le cose anche in coda e alcune informazioni su quanto lavoro è svolto per ciascun lavoro (il thread di lavoro impiega più tempo nell'elaborazione del lavoro o in attesa di lavoro).

Probabilmente la soluzione migliore sarebbe utilizzare un'istanza dell'oggetto da bloccare e utilizzare Monitor.Pulse e Monitor.Wait come variabile di condizione.

Modifica: con una vista del codice da accodare, sembra che la risposta #1116297 abbia ragione: un ritardo di 1 ms è troppo lungo per attendere, dato che molti degli elementi di lavoro saranno estremamente veloci da elaborare.

L'approccio di disporre di un meccanismo per riattivare il thread di lavoro è corretto (poiché non esiste alcuna coda concorrente .NET con un'operazione di rimozione della coda di blocco). Tuttavia, piuttosto che utilizzare un evento, una variabile di condizione sta per essere un po 'più efficiente (come nei casi di non-contesa non richiede una transizione del kernel):

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 rilascerà il blocco sul parametro, attendere fino a Pulse() o PulseAll() viene chiamato contro il blocco, quindi immettere nuovamente il blocco e tornare. È necessario ricontrollare la condizione di attesa perché alcuni altri thread potrebbero aver letto l'elemento fuori dalla coda.

+0

La maggior parte dei lavori corrisponderà solo a un regex (precompilato) e alla chiusura (perché la corrispondenza non è riuscita). Quante dipendono dal numero di utenti inseriti e dalla quantità di dati ricevuti dall'applicazione (è un'app di rete). È molto probabile che possa raggiungere il picco a diverse centinaia di secondi al massimo carico, forse fino a mille. Non ero sicuro se qualcuno fosse interessato alle voci di accodamento del codice, ma ora lo sto modificando, dal momento che l'hai chiesto così gentilmente :) –

+0

Ho pensato di leggere da qualche parte che Monitor era il supporto dietro il blocco() {} costruire? Com'è possibile quindi utilizzare lock() e Monitor sullo stesso oggetto di sincronizzazione del genere? –

+0

Oh aspetta, ho appena letto e capito l'ultimo paragrafo lì. –