Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ManualResetEventSlim: Calling .Set() followed immediately by .Reset() doesn't release *any* waiting threads

ManualResetEventSlim: Calling .Set() followed immediately by .Reset() doesn't release any waiting threads

(Note: This also happens with ManualResetEvent, not just with ManualResetEventSlim.)

I tried the code below in both release and debug mode. I'm running it as a 32-bit build using .Net 4 on Windows 7 64-bit running on a quad core processor. I compiled it from Visual Studio 2012 (so .Net 4.5 is installed).

The output when I run it on my system is:

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.

So setting and then immediately resetting the event did not release a single thread. If you uncomment the Thread.Sleep(), then they are all released.

This seems somewhat unexpected.

Does anyone have an explanation?

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

Note: This question poses a similar problem, but it doesn't seem to have an answer (other than confirming that yes, this can happen).

Issue with ManualResetEvent not releasing all waiting threads consistently


[EDIT]

As Ian Griffiths points out below, the answer is that the underlying Windows API that is used is not designed to support this.

It's unfortunate that the Microsoft documentation for ManualResetEventSlim.Set() states wrongly that it

Sets the state of the event to signaled, which allows one or more threads waiting on the event to proceed.

Clearly "one or more" should be "zero or more".

like image 784
Matthew Watson Avatar asked Feb 25 '13 12:02

Matthew Watson


1 Answers

Resetting a ManualResetEvent is not like calling Monitor.Pulse - it makes no guarantee that it will release any particular number of threads. On the contrary, the documentation (for the underlying Win32 synchronization primitive) is pretty clear that you can't know what will happen:

Any number of waiting threads, or threads that subsequently begin wait operations for the specified event object, can be released while the object's state is signaled

The key phrase here is "any number" which includes zero.

Win32 does provide a PulseEvent but as it says "This function is unreliable and should not be used." The remarks in its documentation at http://msdn.microsoft.com/en-us/library/windows/desktop/ms684914(v=vs.85).aspx provide some insight into why pulse-style semantics cannot reliably be achieved with an event object. (Basically, the kernel sometimes takes threads that are waiting for an event off its wait list temporarily, so it's always possible that a thread will miss a 'pulse' on an event. That's true whether you use PulseEvent or you try to do it yourself by setting and resetting the event.)

The intended semantics of ManualResetEvent is that it acts as a gate. The gate is open when you set it, and is closed when you reset it. If you open a gate and then quickly close it before anyone had a chance to get through the gate, you shouldn't be surprised if everyone is still on the wrong side of the gate. Only those who were alert enough to get through the gate while you held it open will get through. That's how it's meant to work, so that's why you're seeing what you see.

In particular the semantics of Set are very much not "open gate, and ensure all waiting threads are through the gate". (And if it were to mean that, it's not obvious what the kernel should do with multi-object waits.) So this is not a "problem" in the sense that the event isn't meant to be used the way you're trying to use it, so it's functioning correctly. But it is a problem in the sense that you won't be able to use this to get the effect you're looking for. (It's a useful primitive, it's just not useful for what you're trying to do. I tend to use ManualResetEvent exclusively for gates that are initially closed, and which get opened exactly once, and never get closed again.)

So you probably need to consider some of the other synchronization primitives.

like image 126
Ian Griffiths Avatar answered Oct 20 '22 19:10

Ian Griffiths