Saturday, October 9, 2010

Cancelable Alternative to Thread.Sleep Method (C#)

Many times in the past I have seen fragments of code like this one:

private void MyThreadRun(object state)
{
    while (!m_myThreadCanceled)
    {
        // Do some processing here...

        Thread.Sleep(SleepInterval);
    } 
}

public void Cancel()
{
    m_myThreadCanceled = true;
}

private volatile bool m_myThreadCanceled;

What we have here is a thread doing some processing in regular intervals. The reason for not implementing this as a Task is that we have a lot of context (local variables) that we want to keep between iterations. Obviously, we don’t want this thread to run forever, so we want to have a way to cancel it (from another thread) in a safe way. This means that we don’t want to interrupt thread if it is currently in the processing phase (we don’t want to do a dangerous asynchronous thread abort operation). At the moment of cancelation, if the thread is in the processing phase, we want it to exit when it finishes with processing. On the other hand, if the thread is in its waiting phase we want it to exit immediately.

However, the code above doesn’t accomplish that. There is a great chance that we will try to cancel thread in the middle of Thread.Sleep(int) method, which means that thread will have to wait for a Thread.Sleep(int) method to finish before it exits. This is not what we want. Let’s say that we try to terminate our application gracefully (by canceling threads like the one above), that means that termination might take very long (up to SleepInterval)!

Here is a simple solution for this problem:

private void MyThreadRun()
{
    while (!m_lightSleeper.HasBeenCanceled)
    {
       // Do some processing here...

        m_lightSleeper.Sleep(SleepInterval);
    }
}

public void Cancel()
{
    m_lightSleeper.Cancel();
}

private LightSleeper m_lightSleeper = new LightSleeper();

Obviously, we rely on LightSleeper class. Cancel() method cancels the m_lightSleeper object. At the moment of cancelation, if the thread is in its processing phase, it will complete processing, m_lightSleep.Sleep(SleepInterval) will immediately return, while loop will complete and thread will exit. If the thread is in waiting phase, m_lightSleeper.Sleep(SleepInterval) will return immediately, and thread will exit in the same way as in the previous case. This is exactly the kind of behavior that we want.

Now lets describe LightSleeper class.

public class LightSleeper
{
    /// <summary>
    /// Sleeps for the specified amount of time.
    /// It can wake up earlier if Cancel() is called during the sleep.
    /// Also, it returns immediately if Cancel() has already been called.
    /// </summary>
    /// <param name="millisecondsTimeout">Sleep interval in milliseconds.</param>
    /// <returns>True if sleep wasn't canceled, false otherwise.</returns>
    public bool Sleep(int millisecondsTimeout)
    {
        return !m_manualResetEvent.WaitOne(millisecondsTimeout);
    }

    /// <summary>
    /// Cancels the current sleep operation (if there is one in progress),
    /// and causes all future sleep operations to return immediately when called.
    /// </summary>
    public void Cancel()
    {
        // Only one thread calling Cancel() can actually set the event.
        if (Interlocked.Exchange(ref m_canceled, Canceled) == NotCanceled)
        {
            m_manualResetEvent.Set();
        }
    }

    /// <summary>
    /// Returns true if light sleeper has been canceled.
    /// </summary>
    public bool HasBeenCanceled
    {
        get 
        {
            return (m_canceled == Canceled); 
        }
    }

    private const int NotCanceled = 0;
    private const int Canceled = 1;

    private int m_canceled = NotCanceled; 
    private ManualResetEvent m_manualResetEvent = new ManualResetEvent(false);
}

The roles of all methods are described by documentation comments, but let’s discuss this class in more detail. Sleep(int) method waits on a ManualResetEvent. Therefore if Cancel() – which sets this event – has already been called, WaitOne() method will return immediately. If Cancel() hasn’t been called, it will start sleeping, but will wake up if Cancel() is called during the sleep. In the last scenario Cancel() won’t be called during the Sleep(int) operation, and Sleep(int) will return after the given millisecondsTimeout expires. Sleep(int) method returns true if it wasn’t canceled, otherwise it returns false.

Cancel() method is used to wake up current thread from the sleep (by setting m_manualResetEvent) and to set HasBeenCanceled property to true. Case where several threads try to cancel current thread is valid, and therefore by using Interlocked.CompareExchange(ref int, int) method we make sure that only one thread can actually cancel the LightSleeper.

HasBeenCanceled property, as expected, returns true if LightSleeper has been canceled. It does so by reading m_canceled property. Here we want to return the freshest value of m_canceled variable. There is a possibility that the current value of m_canceled read from the current processor’s cache is not up to date. Normally, we would call Thread.VolatileRead(ref m_canceled) to achieve this, but in this case it is not necessary, since Interlocked.CompareExchange method made sure that the variable has been updated immediately in caches of all processors. Therefore, here we gain a bit on performance by not using the more expensive method.

Reason for m_canceled being int is that Interlocked.CompareExchange() method doesn’t accept bool as an argument.

Also, you might notice that LightSleeper doesn’t implement IDisposable interface. This class uses a ManualResetEvent which is disposable class, so it is recommended that it also implements disposable pattern. However, LightSleeper is used by at least two threads and can could cause ObjectDisposedExceptions if the “cancelable” thread completes before Cancel() was called by “canceling” thread. In this particular case lightSleeper object could be disposed when the “cancelable” thread completes, so “canceling” thread would try to call Cancel() on disposed object. We simplify the problem by not implementing IDisposable. Threfore, if Cancel() is called after “cancelable” thread completes it won’t have any effect.

1 comment:

Anonymous said...

Thank you very much for this code sample. It is incredibly useful, because of the type of threading that I do. I now have full control over my threading because of your class. You made my day.