Вопрос по multithreading, c#, manualresetevent, sleep – ManualResetEvent против Thread.Sleep

12

Я реализовал следующий поток фоновой обработки, где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 достичь такого уровня производительности? ».

EDIT: Так как кто-то спросил о функции, которая ставит в очередь элементы, она здесь, вместе со всей системой, какой она есть в данный момент.

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

Ваш Ответ

2   ответа
16

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

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

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

Более новая информация: минимальное разрешение 10 мс - это XP и более ранняя вещь, поскольку ОС использовала статические приращения 10 мс для планирования. Я думаю, что Vista, и я знаю, что Win7 использует, динамический «тик тик» временной интервал С Win7 я могу запустить таймер с высоким разрешением, вывести режим сна (1), и время очень близко к 1 мс, иногда меньше.
10

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

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

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

Ой, подождите, я только что прочитал и понял последний абзац там. Matthew Scharley
Мне показалось, что я где-то читал, что Monitor является основой конструкции lock () {}? Почему же вы можете использовать lock () и Monitor для одного и того же объекта синхронизации? Matthew Scharley
Большинство заданий будут просто соответствовать (скомпилированному) регулярному выражению и выходить (потому что совпадение не удалось). Сколько зависит от того, сколько пользователь вводит и сколько данных получает приложение (это сетевое приложение). Вполне возможно, что при максимальной нагрузке он может достигать пика в несколько сотен секунд, возможно, до тысячи. Я не был уверен, что кто-нибудь заинтересуется элементами очереди кода, но я сейчас редактирую это, поскольку вы так мило спросили :) Matthew Scharley

Похожие вопросы