ManualResetEventSlim: Вызов .Set (), за которым следует сразу .Reset () не выпускает * любые * ожидающие streamи

ManualResetEventSlim: вызов .Set (), за которым следует немедленно .Reset (), не освобождает ожидающие streamи

(Примечание. Это также происходит с ManualResetEvent , а не только с ManualResetEventSlim .)

Я попробовал код ниже как в режиме выпуска, так и в режиме отладки. Я запускаю его как 32-битную сборку с использованием .Net 4 на 64-разрядной версии Windows 7 на четырехъядерном процессоре. Я скомпилировал его из Visual Studio 2012 (поэтому установлен .Net 4.5).

Выход, когда я запускаю его в своей системе:

 Waiting for 20 threads to start Thread 1 started. Thread 2 started. Thread 3 started. Thread 4 started. Thread 0 started. Thread 7 started. Thread 6 started. Thread 5 started. Thread 8 started. Thread 9 started. Thread 10 started. Thread 11 started. Thread 12 started. Thread 13 started. Thread 14 started. Thread 15 started. Thread 16 started. Thread 17 started. Thread 18 started. Thread 19 started. Threads all started. Setting signal now. 0/20 threads received the signal. 

Поэтому настройка, а затем сразу же сброс события не выдает ни одного streamа. Если вы раскомментируете Thread.Sleep (), все они будут выпущены.

Это кажется несколько неожиданным.

У кого-нибудь есть объяснение?

 using System; using System.Threading; using System.Threading.Tasks; namespace Demo { public static class Program { private static void Main(string[] args) { _startCounter = new CountdownEvent(NUM_THREADS); // Used to count #started threads. for (int i = 0; i  test(id)); } Console.WriteLine("Waiting for " + NUM_THREADS + " threads to start"); _startCounter.Wait(); // Wait for all the threads to have called _startCounter.Signal() Thread.Sleep(100); // Just a little extra delay. Not really needed. Console.WriteLine("Threads all started. Setting signal now."); _signal.Set(); // Thread.Sleep(50); // With no sleep at all, NO threads receive the signal. _signal.Reset(); Thread.Sleep(1000); Console.WriteLine("\n{0}/{1} threads received the signal.\n\n", _signalledCount, NUM_THREADS); Console.WriteLine("Press any key to exit."); Console.ReadKey(); } private static void test(int id) { Console.WriteLine("Thread " + id + " started."); _startCounter.Signal(); _signal.Wait(); Interlocked.Increment(ref _signalledCount); Console.WriteLine("Task " + id + " received the signal."); } private const int NUM_THREADS = 20; private static readonly ManualResetEventSlim _signal = new ManualResetEventSlim(); private static CountdownEvent _startCounter; private static int _signalledCount; } } 

Примечание. Этот вопрос задает аналогичную проблему, но, похоже, у него нет ответа (кроме подтверждения того, что да, это может произойти).

Проблема с ManualResetEvent, не отделяющая все ожидающие streamи последовательно


[РЕДАКТИРОВАТЬ]

Как отмечает Йен Гриффитс, ответ заключается в том, что базовый Windows API, который используется, не предназначен для поддержки этого.

К сожалению, документация Microsoft для ManualResetEventSlim.Set () неверно заявляет, что она

Устанавливает состояние события для сигнализации, что позволяет продолжить один или несколько streamов, ожидающих события.

Очевидно, что «один или несколько» должны быть «ноль или больше».

Сброс ManualResetEvent не похож на вызов Monitor.Pulse – он не гарантирует, что он выпустит какое-то определенное количество streamов. Напротив, документация (для базового примитива синхронизации Win32) довольно понятна, что вы не можете знать, что произойдет:

Любое количество ожидающих streamов или streamов, которые впоследствии начинают операции ожидания для указанного объекта события, может быть освобождено, когда состояние объекта сигнализируется

Ключевая фраза здесь – «любое число», которое включает ноль.

Win32 действительно предоставляет PulseEvent но поскольку он говорит: «Эта функция ненадежна и не должна использоваться». Замечания в своей документации на http://msdn.microsoft.com/en-us/library/windows/desktop/ms684914(v=vs.85).aspx дают некоторое представление о том, почему семантика в стиле импульса не может быть надежно достигнута с помощью объект события. (В основном, kernel ​​иногда принимает streamи, которые ждут события из своего списка ожидания временно, поэтому всегда возможно, что stream будет пропускать «пульс» в событии. Это правда, используете ли вы PulseEvent или пытаетесь сделать это сами путем установки и сброса события.)

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

В частности, семантика Set очень не «открытые ворота», а все ожидающие streamи проходят через ворота ». (И если бы это означало это, неясно, что kernel ​​должно делать с ожиданиями нескольких объектов.) Таким образом, это не «проблема» в том смысле, что событие не предназначено для использования так, как вы пытаясь использовать его, поэтому он работает правильно. Но это проблема в том смысле, что вы не сможете использовать это, чтобы получить эффект, который вы ищете. (Это полезный примитив, это просто не полезно для того, что вы пытаетесь сделать. Я склонен использовать ManualResetEvent исключительно для закрытых ворот, которые открываются ровно один раз и никогда не закрываются снова.)

Поэтому вам, вероятно, нужно рассмотреть некоторые из других примитивов синхронизации.