Подтвердить что ты не робот

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

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

(Примечание: это также происходит с 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.

Таким образом, установка, а затем сразу же сброс события не выпустила нить. Если вы раскомментируете 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 < NUM_THREADS; ++i)
            {
                int id = i;
                Task.Factory.StartNew(() => 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, не отделяющая все ожидающие потоки последовательно


[EDIT]

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

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

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

Ясно, что "один или несколько" должны быть "ноль или больше".

4b9b3361

Ответ 1

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

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

Ключевая фраза здесь - "любое число", которое включает нуль.

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

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

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

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