Tuesday 16 December 2014

ManualResetEvent and ManualResetEventSlim (.NET)

Starting with .NET 4.0 the framework provides a ManualResetEventSlim class to compliment the existing ManualResetEvent. ManualResetEventSlim is much more efficient for short wait times. It can be used when the event doesn't cross process boundaries so if this is the case, and you don't expect threads to be blocked for long, then ManualResetEventSlim is the better option.

The efficiency improvement of ManualResetEventSlim is achieved because when it enters a Wait() (equivalent to ManualResetEvent.WaitOne()) it initially spins in a busy loop for a short time while waiting for the event to become signalled. This StackOverflow answer explains how the spinning is done using a combination of Thread.Yield, Thread.SpinWait, Thread.Sleep(0) and Thread.Sleep(1). If the event is signalled during this period then the wait returns faster than the ManualResetEvent, which is implemented using a Monitor.Wait.

If the event doesn't occur within a certain short time then the ManualResetEventSlim switches to use a Monitor.Wait just like the ManualResetEvent. Note that the initial spinning is CPU intensive, so if you are sure that wait times will be longer you can save CPU cycles by using a ManualResetEvent and performance won't suffer.

Illustration by Example

To illustrate the potential performance improvement consider the following code. Two tasks are created which progress around a loop 1 million times in tandem as they signal each-other to continue.

var numRepeats = 1000000;
var stopwatch = new Stopwatch();
var firstTask = new ManualResetEventSlim(false);
var secondTask = new ManualResetEventSlim(false);
var firstTaskComplete = new ManualResetEvent(false);

Task.Factory.StartNew(() => {
    for (var x = 0; x < numRepeats; x++) {
        firstTask.Wait();
        firstTask.Reset();
        secondTask.Set();
    }
    firstTaskComplete.Set();
});

Task.Factory.StartNew(() => {
    for (var x = 0; x < numRepeats; x++) {
        secondTask.Wait();
        secondTask.Reset();
        firstTask.Set();
    }
});

stopwatch.Start();
firstTask.Set();
firstTaskComplete.WaitOne();
stopwatch.Stop();
Console.WriteLine("Total time: " + stopwatch.Elapsed);

Using ManualResetEventSlim as shown the above executes in about 0.52~0.55 seconds on my machine. If I switch to using ManualResetEvent then the same task takes about 11~12 times longer, 5.8~6.4 seconds.

If I leave the events signaled for the entire task without setting or resetting them, so the waits still check the manual reset event but are never blocked, then the difference between ManualResetEventSlim and ManualResetEvent is much more prominent, up to 40x.

No Equivalent AutoResetEventSlim?

There's currently no slim version of AutoResetEvent as it's difficult to think of a time when it would be useful. If you're using AutoResetEvent it's probably because you want to give threads exclusive access to a resource to carry out some work. Chances are this work will take some non-zero amount of time, in which case it would be wasteful for other threads to be spinning while they wait for their signal to continue. Therefore, a slim version of AutoResetEvent is unlikely to be useful. For more info see this discussion.