ManualResetEvent.WaitOne() не возвращает значение, если Reset() вызывается сразу после Set()

У меня есть проблема в производственной службе, которая содержит «сторожевой» таймер, используемый для проверки того, не зависло ли основное задание обработки (это связано с проблемой COM-взаимодействия, которую, к сожалению, нельзя воспроизвести в тесте).

Вот как это работает в настоящее время:

  • Во время обработки основной поток сбрасывает ManualResetEvent, обрабатывает один элемент (это не должно занять много времени), затем устанавливает событие. Затем он продолжает обрабатывать все оставшиеся элементы.
  • Каждые 5 минут сторожевой таймер вызывает WaitOne(TimeSpan.FromMinutes(5)) по этому событию. Если результат ложный, служба перезапускается.
  • Иногда во время нормальной работы служба перезапускается этим сторожевым таймером, хотя обработка не занимает и пяти минут.

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

Насколько я понимаю WaitOne(), заблокированный поток гарантированно получит сигнал, когда Set() вызывается, но я предполагаю, что упускаю что-то важное.

Обратите внимание, что если я разрешаю переключение контекста, вызывая Thread.Sleep(0) после вызова Set(), WaitOne() никогда не завершается ошибкой.

Ниже приведен пример, который обеспечивает то же поведение, что и мой производственный код. WaitOne() иногда ждет 5 секунд и завершается ошибкой, даже если Set() вызывается каждые 800 миллисекунд.

private static ManualResetEvent _handle;

private static void Main(string[] args)
{
    _handle = new ManualResetEvent(true);

    ((Action) PeriodicWait).BeginInvoke(null, null);
    ((Action) PeriodicSignal).BeginInvoke(null, null);

    Console.ReadLine();
}

private static void PeriodicWait()
{
    Stopwatch stopwatch = new Stopwatch();

    while (true)
    {
        stopwatch.Restart();
        bool result = _handle.WaitOne(5000, false);
        stopwatch.Stop();
        Console.WriteLine("After WaitOne: {0}. Waited for {1}ms", result ? "success" : "failure",
                            stopwatch.ElapsedMilliseconds);
        SpinWait.SpinUntil(() => false, 1000);
    }
}

private static void PeriodicSignal()
{
    while (true)
    {
        _handle.Reset();
        Console.WriteLine("After Reset");
        SpinWait.SpinUntil(() => false, 800);
        _handle.Set();
        // Uncommenting either of the lines below prevents the problem
        //Console.WriteLine("After Set");
        //Thread.Sleep(0);
    }
}

вывод вышеуказанного кода


Вопрос

Хотя я понимаю, что вызов Set(), за которым следует Reset(), не гарантирует, что все заблокированные потоки будут возобновлены, не гарантируется ли также и то, что любые ожидающие потоки будут освобождены?


person Simon MᶜKenzie    schedule 20.03.2013    source источник
comment
Thread.Sleep(0) не обязательно вызывает переключение контекста — он переключается только на потоки с таким же приоритетом. Thread.Sleep(1) переключится на потоки с другим приоритетом, если они ожидают. См. bluebytesoftware.com/blog/   -  person Peter Ritchie    schedule 20.03.2013
comment
@PeterRitchie, вот почему я сказал разрешить переключение контекста! В данном случае это определенно имеет значение!   -  person Simon MᶜKenzie    schedule 20.03.2013
comment
В документах также говорится, что если метод Reset вызывается до того, как все потоки возобновят выполнение, оставшиеся потоки снова блокируются msdn.microsoft.com/en-us/library/ksb7zs2x(v=vs.95).aspx. В нем также указано, что события автоматического сброса не гарантируют разблокировку потока примерно в тех же условиях (msdn.microsoft.com/en-us/library/)   -  person Peter Ritchie    schedule 20.03.2013
comment
Мое типичное использование событий ручного сброса: один поток вызывает Set, а другой поток вызывает Reset. вызов Set, а затем немедленный вызов Reset не кажется хорошей идеей, основываясь на документации.   -  person Peter Ritchie    schedule 20.03.2013
comment
@PeterRitchie, но разве не разумно ожидать, что будет выпущен хотя бы один поток? Читая документацию, я понял, что не могу гарантировать, что все потоки разблокируются, а не что я не могу гарантировать каждый...   -  person Simon MᶜKenzie    schedule 20.03.2013
comment
нет, это в основном работает в квантовом режиме. Он не может украсть время пользовательского потока, чтобы запланировать другой поток. Он выделяет и планирует потоки на квантовой частоте. Если вы переключите сигнал в пределах этого кванта, он может никогда его не увидеть.   -  person Peter Ritchie    schedule 20.03.2013
comment
Это всплывало раньше. Похоже, что конечный автомат ОС сломан. Set делает потоки готовыми, но не удаляет их из очереди ожидания MRE до тех пор, пока они не начнут выполняться. Если сброс происходит, когда поток все еще готов и, следовательно, еще не запущен, он снова возвращается в состояние ожидания. Я не думаю, что такое поведение вменяемо, но так оно и есть.   -  person Martin James    schedule 20.03.2013
comment
ИМХО, вызов set на MRE должен сделать все текущие ожидающие потоки готовыми и удалить их из очереди ожидания MRE, чтобы последующий сброс не мог изменить их состояние обратно с готовности на ожидание. Я не могу придумать никакой причины, по которой OS/MRE ведет себя так, как она это делает. Это просто глупо.   -  person Martin James    schedule 20.03.2013
comment
@PeterRitchie - это правильно? Если сигнал делает другой поток готовым, почему поток не может быть запущен во время вызова, предполагая, что для него доступно ядро ​​с его текущим приоритетом (возможно, плюс временное повышение приоритета). Если во время вызова необходимо вытеснить текущий поток, то почему бы и нет? При чем здесь «квант»? Мне кажется, что если этого не сделать, то в первую очередь будет отброшена основная причина использования упреждающего планировщика (высокая производительность ввода-вывода).   -  person Martin James    schedule 20.03.2013
comment
@MartinJames Это невозможно сделать во время вызова, потому что это не поток ОС, а пользовательский поток. Приложение создало это, чтобы выполнять свою работу, а не ОС. Windows — это операционная система с упреждающей операцией по расписанию, когда время получения потока выполняется по расписанию. Я поддерживаю рекомендацию по параллельному программированию в Windows, вы получите гораздо лучшее понимание того, что на самом деле происходит.   -  person Peter Ritchie    schedule 20.03.2013
comment
@PeterRitchie - ну, вызов set должен выходить через планировщик, как и любой обычный системный вызов, который может изменить состояние потоков. Какое может быть оправдание для откладывания полной реализации любого такого призыва на потом?   -  person Martin James    schedule 20.03.2013
comment
@PeterRitchie, просто для справки в будущем, Sleep(1) больше не требуется в версиях ОС после Server 2003.   -  person Simon MᶜKenzie    schedule 24.04.2014


Ответы (2)


Нет, это принципиально сломанный код. Есть только разумные шансы, что WaitOne() завершится, если вы сохраните MRE установленным в течение такого короткого промежутка времени. Windows предпочитает освобождать поток, заблокированный по событию. Но это резко потерпит неудачу, если поток не ждет. Или вместо этого планировщик выбирает другой поток, который выполняется с более высоким приоритетом и также был разблокирован. Например, это может быть поток ядра. MRE не хранит «память» о том, что сигнал был получен, но его еще не ждали.

Ни Sleep(0), ни Sleep(1) недостаточно хороши, чтобы гарантировать завершение ожидания, нет разумной верхней границы того, как часто планировщик может обходить ожидающий поток. Хотя вам, вероятно, следует закрыть программу, если она занимает более 10 секунд;)

Вам нужно будет сделать это по-другому. Простой способ — положиться на работника, чтобы в конечном итоге установить событие. Поэтому сбросьте его, прежде чем начать ждать:

private static void PeriodicWait() {
    Stopwatch stopwatch = new Stopwatch();

    while (true) {
        stopwatch.Restart();
        _handle.Reset();
        bool result = _handle.WaitOne(5000);
        stopwatch.Stop();
        Console.WriteLine("After WaitOne: {0}. Waited for {1}ms", result ? "success" : "failure",
                            stopwatch.ElapsedMilliseconds);
    }
}

private static void PeriodicSignal() {
    while (true) {
        _handle.Set();
        Thread.Sleep(800);   // Simulate work
    }
}
person Hans Passant    schedule 20.03.2013

Вы не можете «пульсировать» событие ОС, подобное этому.

Среди других проблем есть тот факт, что любой поток ОС, выполняющий ожидание блокировки дескриптора ОС, может быть временно прерван APC режима ядра; когда APC завершается, поток возобновляет ожидание. Если импульс произошел во время этого прерывания, поток его не увидит. Это всего лишь один пример того, как можно пропустить «импульсы» (подробно описано в Параллельное программирование в Windows, стр. 231).

Кстати, это означает, что PulseEvent Win32 API полностью сломан.

В среде .NET с управляемыми потоками еще больше шансов пропустить импульс. Вывоз мусора и др.

В вашем случае я бы подумал о переключении на AutoResetEvent, который многократно Set выполняется рабочим процессом и (автоматически) сбрасывается сторожевым процессом каждый раз, когда его Wait завершается. И вы, вероятно, захотите «приручить» сторожевой таймер, заставляя его проверять только каждую минуту или около того.

person Stephen Cleary    schedule 20.03.2013