ManualResetEvent против Thread.Sleep

Я реализовал следующий поток фоновой обработки, где Jobs - Queue<T>:

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.
        }
    }
}

Это вызвало заметную задержку между тем, когда были введены задания и когда они фактически запускались (сразу же загружаются партии заданий, и каждая работа является только [относительно] небольшой.) Задержка не была огромная сделка, но я подумал о проблеме и сделал следующее изменение:

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

Если теперь задания добавления потоков блокируются _workerWait и вызывает _workerWait.Set(), когда они добавили задания. Это решение (по-видимому) мгновенно начинает обработку заданий, и задержка вообще уходит.

Мой вопрос частично "Почему это происходит?", при условии, что Thread.Sleep(int) может очень хорошо спать дольше, чем вы указываете, и отчасти "Как достичь ManualResetEvent этого уровня производительности?".

РЕДАКТИРОВАТЬ: Так как кто-то спросил о функции, которая размещает элементы в очереди, вот она вместе с полной системой, поскольку она стоит на данный момент.

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

Ответ 1

События - это примитивы ядра, предоставляемые OS/Kernel, которые предназначены только для такого рода вещей. Ядро предоставляет границу, на которой вы можете гарантировать атомные операции, которые важны для синхронизации (некоторые атомы могут быть сделаны в пользовательском пространстве тоже с поддержкой аппаратного обеспечения).

Короче говоря, когда поток ожидает событие, он помещает список ожиданий для этого события и помечен как non-runnable. Когда событие сигнализируется, ядро ​​просыпает те, которые находятся в списке ожидания, и маркирует их как runnable, и они могут продолжать работать. Естественно, огромная выгода, при которой поток может сразу проснуться, когда событие сигнализируется, а также долгое время спящего режима и периодически перепроверять это условие.

Даже одна миллисекунда действительно очень долгое время, вы могли обработать тысячи событий за это время. Также временное разрешение традиционно составляет 10 мс, поэтому спать менее 10 мс, как правило, приводит к спаду в 10 мс. С событием поток можно разбудить и запланировать немедленно

Ответ 2

Первая блокировка на _workerWait бессмысленна, событие - это системный (ядро) объект, предназначенный для сигнализации между потоками (и сильно используемый в Win32 API для асинхронных операций). Поэтому совершенно безопасно устанавливать несколько потоков или reset без дополнительной синхронизации.

Что касается вашего основного вопроса, вам нужно увидеть логику размещения вещей в очереди, а также некоторую информацию о том, как много работы делается для каждой работы (рабочий поток тратит больше времени на обработку или на ожидание работы).

Вероятнее всего, лучшим решением будет использование экземпляра объекта для блокировки и использования Monitor.Pulse и Monitor.Wait в качестве переменной условия.

Изменить: с учетом кода для очереди, кажется, что ответ #1116297 имеет право: задержка в 1 мс слишком долго ждать, учитывая, что многие из рабочих элементов будут чрезвычайно быстрыми для обработки.

Подход к созданию механизма для пробуждения рабочего потока является правильным (так как не существует параллельной очереди .NET с операцией блокировки блокировки). Однако вместо того, чтобы использовать событие, переменная состояния будет немного более эффективной (так как в непротиворечивых случаях она не требует перехода ядра):

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 отпустит блокировку параметра, дождитесь, пока Pulse() или PulseAll() вызывается против блокировки, затем повторно введите блокировку и возврат. Необходимо перепроверить условие ожидания, потому что какой-то другой поток мог прочитать элемент из очереди.