Wednesday, March 14, 2012

Using AutoResetEvent for controlling .NET Windows Service Start and Stop

Problem:
You have to implement a polling service that does a certain task every 'n' units of time till it is stopped.

Pretext:
The methods of interest in this problem are OnStart, OnStop and the real worker thread that we implement. So I would be concentrating on these and will skip out on the boiler plate code required to setup the service. I would be demonstrating only the relevant parts of code in this discussion. Also, the example code here would lack exception handling, thread syncing, for the sake of staying on track with explaining the better solution.

Solutions:
1. Implement a worker thread that does the actual work. Inside the worker thread, implement a sleep loop.

Here is some sample code demonstrating this:

public partial class SampleService : ServiceBase
{
           Thread _worker;
           bool _shouldStop;
           //service start
          protected override void OnStart(string[] args)
         {
                    _worker = new Thread(new ThreadStart(DoWork));
                    _worker.Start();
         }

        //service stopped
        protected override void OnStop()
        {
               _shouldStop = true;
        }

        private void DoWork()
        {
               while(!_shouldStop)
               {
                    //do work here
                    Thread.Sleep(1000); //using 1 sec here. but this can be any configurable value.
               }
        }
}

The basic problem with this solution is as follows:
If the sleep time was a larger value (which is the most common case), for example 5 minutes or 300 secs, then the service would stop responding if someone stopped the service while it was in the middle of the sleep statement. Essentially what happens is that the operating system waits for the worker thread to complete before it can dispose off the service process. Because the worker thread is in a sleep mode, and there is no implementation done to interrupt it, it never returns before the sleep timeout is over. Hence, after a certain period of time, the service goes in a not responding state.
This is just a basic explanation of what might happen in this case, there are other scenarios that might happen with different outcomes, but this is the most common one.

2. Use AutoResetEvent to control the service and the worker thread. This is a better approach, as this gives more control on the service lifetime and allows a clean way to stop the service. 

Here is some sample code demonstrating this approach:

   //Helper class, that wraps the AutoResetEvents.
   //Having this class makes the code a bit cleaner and easier to understand structurally.

    public class ThreadHelper
    {
        public ThreadHelper() { }

        //false in the constructor below signifies not signaled state
        private AutoResetEvent _stopEvent = new AutoResetEvent(false);
        private AutoResetEvent _waitToFinishEvent = new AutoResetEvent(false);
        private Thread _thread = null;

        public AutoResetEvent stopEvent
        {
            set { _stopEvent = value; }
            get { return _stopEvent; }
        }
        public AutoResetEvent waitToFinishEvent
        {
            set { _waitToFinishEvent = value; }
            get { return _waitToFinishEvent; }
        }
        public Thread thread
        {
            set { _thread = value; }
            get { return _thread; }
        }
    }



public partial class SampleService : ServiceBase
{
           ThreadHelper _worker = new ThreadHelper();

           //service start
          protected override void OnStart(string[] args)
         {
                    _worker.thread = new Thread(new ThreadStart(DoWork));
                    _worker.thread.Start();
         }

        //service stopped
        protected override void OnStop()
        {
             if ((_worker.thread != null) && (_worker.thread.IsAlive == true) && (_worker.stopEvent != null))
            {
                 _worker.stopEvent.Set(); //signal DoWork thread to stop
                 //15000 [15 secs] can be changed to any value that you want to let the worker thread finish
                 if (!_worker.waitToFinishEvent.WaitOne(15000, false))
                {
                       //Even after waiting 15000 [15 secs] (or any value that you choose), the worker did not finish
                      //cannot wait any more, so abort the worker thread
                       _worker.thread.Abort();
                }
            }
        }

        private void DoWork()
        {
               bool shouldStop = false;
               while(!shouldStop)
               {
                        if (_worker.stopEvent.WaitOne(/*your polling time*/, false))
                       {
                             shouldStop = true;
                            _worker.waitToFinishEvent.Set();
                       }
                       else
                       {
                             //do work here
                       }
               }
        }
}


An interesting thing to note is that OnStart and OnStop are on the main thread and DoWork is on another thread. Also, the WaitOne method returns true if a signal was received and false if it was not. Specifying a wait time as the first parameter to the WaitOne method causes it to wait for a signal till the wait time is completed. If a signal is not received within the specified wait time, it returns false.

In essence, what happens in this case is that OnStop, signals the stopEvent, which causes the WaitOne (in DoWork thread) to return true (because it was signaled). This causes the shouldStop to be set to true and we come out of the while loop causing the method and hence the thread to complete. On the other hand, after setting shouldStop to true, we signal waitToFinish, which causes the OnStop to complete [because WaitOne in the OnStop returns true and the statements inside if are not evaluated]. In case, if our worker thread was doing the work and it took longer than 15 sec to respond the OnStop WaitOne would return false and it would abort the worker thread.
In any case, this would never cause our service to go in a not responding state.

Although, the explanation here might seem a bit complex, relating with the sample code should help make the concept more clearer.

Happy Coding!!

Make Custom Gifts at CafePress

No comments:

Post a Comment