2009-07-12 8 views
12

que implementa el siguiente hilo de procesamiento en segundo plano, donde Jobs es una 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. 
     } 
    } 
} 

Esto produjo un retraso notable entre el momento estaban siendo introducidos los puestos de trabajo y, cuando en realidad estaban empezando a ser ejecutar (los lotes de trabajos se ingresan a la vez, y cada trabajo es solo [relativamente] pequeño). La demora no fue un gran problema, pero llegué a pensar en el problema e hice el siguiente cambio:

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

Donde el hilo la adición de trabajos ahora bloquea _workerWait y llama al _workerWait.Set() cuando termina de agregar trabajos. Esta solución (aparentemente) instantáneamente comienza a procesar trabajos, y la demora se ha ido por completo.

Mi pregunta es en parte "¿Por qué sucede esto?", Dado que Thread.Sleep(int) puede dormir mucho más de lo que especifique, y en parte "¿Cómo logra el ManualResetEvent este nivel de rendimiento?".

EDIT: Dado que alguien le preguntó acerca de la función que está haciendo cola de elementos, aquí está, junto con el sistema completo tal como está en este 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); 
      } 
     } 
    } 
} 

Respuesta

15

Los eventos son primitivas del kernel proporcionadas por OS/Kernel diseñadas para este tipo de cosas. El núcleo proporciona un límite sobre el cual puede garantizar operaciones atómicas que son importantes para la sincronización (también se puede hacer algo de atomicidad en el espacio del usuario con soporte de hardware).

En resumen, cuando un hilo espera en un evento, se pone en una lista de espera para ese evento y se marca como no ejecutable. Cuando se señala el evento, el kernel se despierta a los que están en la lista de espera y los marca como ejecutables y pueden continuar ejecutándose. Naturalmente, es un gran beneficio que un hilo se pueda despertar inmediatamente cuando se señala el evento, que duerma durante un tiempo prolongado y vuelva a verificar la condición de vez en cuando.

Incluso un milisegundo es realmente un tiempo muy largo, podría haber procesado miles de eventos en ese momento. Además, la resolución temporal es tradicionalmente de 10ms, por lo que dormir menos de 10ms generalmente da como resultado un sueño de 10ms de todos modos. Con un evento, un subproceso se puede activar y programar inmediatamente

+2

Información más reciente: la resolución mínima de 10 ms es una XP y algo más temprano ya que el sistema operativo usa incrementos estáticos de 10 ms para la programación. Creo que Vista, y sé que Win7 sí, usa un segmento de tiempo dinámico "sin tictac". Con Win7, puedo iniciar un temporizador de alta resolución, emite un sueño (1), y el tiempo es extremadamente cercano a 1 ms, a veces menor que. – Bengie

10

Primera de bloqueo en _workerWait es inútil, un evento es un objeto del sistema (kernel), diseñado para la señalización entre hilos (y muy usado en la API de Win32 para operaciones asíncronas). Por lo tanto, es bastante seguro que varios subprocesos lo configuren o restablezcan sin sincronización adicional.

En cuanto a su pregunta principal, necesita ver la lógica para colocar cosas en la cola y cierta información sobre cuánto trabajo se hace para cada trabajo (el hilo de trabajo pasa más tiempo procesando el trabajo o esperando trabajo).

Probablemente la mejor solución sería usar una instancia de objeto para bloquear y usar Monitor.Pulse y Monitor.Wait como una variable de condición.

Editar: Con la vista del código para en cola, parece que la respuesta #1116297 tiene razón: un retraso de 1ms es demasiado largo de esperar, dado que muchos de los elementos de trabajo serán extremadamente rápidos de procesar.

El enfoque de tener un mecanismo para activar el hilo de trabajo es correcto (ya que no hay una cola concurrente de .NET con una operación de bloqueo de cola). Sin embargo en lugar de utilizar un evento, una condición variable va a ser un poco más eficiente (como en los casos no afirmado que no requiere una transición del núcleo):

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 liberará el bloqueo en el parámetro, espere hasta que Pulse() o PulseAll() se invoque contra el bloqueo, luego vuelva a ingresar el bloqueo y vuelva. Necesita volver a verificar la condición de espera porque algún otro subproceso podría haber leído el elemento fuera de la cola.

+0

La mayoría de los trabajos coincidirán con un Regex (precompilado) y se abandonará (porque la coincidencia falló). El número depende de la cantidad de usuarios que ingresa y de la cantidad de datos que recibe la aplicación (es una aplicación de red). Es muy posible que alcance un pico de varios cientos por segundo a la carga máxima, tal vez hasta mil. No estaba seguro si alguien estaría interesado en los elementos de cola de código, pero ahora lo estoy editando, ya que me lo preguntaste muy bien :) –

+0

Creo que leí en alguna parte que Monitor era el respaldo detrás del bloqueo() {} ¿construir? ¿Cómo es entonces que puedes usar lock() y Monitor en el mismo objeto de sincronización así? –

+0

Oh, espera, acabo de leer y entendí el último párrafo allí. –

Cuestiones relacionadas