Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Gracefully Terminating Threads

5.00/5 (3 votes)
1 Mar 2016CPOL1 min read 12.4K  
This tip will help you achieve a graceful termination of your spawned threads in your ASP.NET application.

Introduction

This tip will help you understand and stress the importance of gracefully terminating running threads, spawned/ started by your ASP.NET application.

Background

Our project wanted that the workers/threads started by the Service (Integration Service- windows service, in our project, that ran several worker threads), exit gracefully when the Service Stop was issued or when it stopped due to some error.

Furthermore, in order to reduce memory leaks of the running application, specifically, when a "STOP" to the service was issued, wanted to ensure that the running threads which are already started do not just be in an inconsistent state, but gracefully exit before termination.

Using the Code

C#
public partial class OMSIntegration : ServiceBase
{
    // constructor
    public OMSIntegration()
    {
         InitializeComponent();
    }

    private Thread syncThread; //processes queue sync requests
    private Thread workflowThread;
    private SyncWorker syncWorker;
    private WorkflowWorker workflowWorker;
    private static Logger logger = LogManager.GetCurrentClassLogger(); // our custom Logging class

    //code for OnStart of the windows Service (called Integration Service)
    protected override void OnStart(string[] args)
    {
      //indicate the service is running
      IsServiceRunning = true;


      //initialize the sync worker
      syncWorker = new SyncWorker(); //This is a class file with custom logic
      syncThread = new Thread(syncWorker.Process);
      if (! syncThread.IsAlive)
      {
       syncThread.Start(this);
       logger.Info("OnStart: SyncWorker thread initialized.");
      }

     //initialize the workflow worker
     workflowWorker = new WorkflowWorker(); //This is a class file with custom logic
     workflowThread = new Thread(workflowWorker.Process);
     if (! workflowThread.IsAlive)
     {
      workflowThread.Start(this);
      logger.Info("OnStart: WorkflowWorker thread initialized.");
     }

     //indicate service is started
     logger.Info("OnStart: OMSIntegrationService is started.");

      //Thread.Sleep(5000); 
      //OnStop();  // Issue Stop after 5 secs. when testing locally
    }

     protected override void OnStop()
     {
       IsServiceRunning = false;
       logger.Info("OnStop: OMSIntegrationService is stopping.");
       JoinThreads();
       base.OnStop();
     }

     /// <summary>
     /// Gets AdditionalTime value from Config, 
     /// requests stop for workers, calls to check if AdditionalTime is required
     /// </summary>
     private void JoinThreads()
     {
        int extraTime = System.Convert.ToInt32
        (ConfigurationManager.AppSettings["ServiceAdditionalTimeInSecs"]) * 1000; //5 secs;

        syncWorker.RequestStop();
        syncThread.Join(extraTime);
        Task.Factory.StartNew(() => IsThread_Running(syncThread,extraTime));

        workflowWorker.RequestStop();
        workflowThread.Join(extraTime);
        Task.Factory.StartNew(() => IsThread_Running(workflowThread, extraTime));

      }

      /// <summary>
      /// Checks to see if the passed Thread is Running, 
      /// if so asks for AdditionalTime for processing
      /// </summary>
      /// <param name="thread">Thread</param>
      /// <param name="timeOut">int</param>
      private void IsThread_Running(Thread thread,int timeOut)
      {
          while(thread.ThreadState == System.Threading.ThreadState.Running && 
                thread.ThreadState != System.Threading.ThreadState.WaitSleepJoin && 
                thread.ThreadState != System.Threading.ThreadState.Stopped)
          {
             RequestAdditionalTime(timeOut);
          }
      }
}

//Sync Worker class file
public class SyncWorker : WorkerBase
{
     private volatile bool _shouldStop; //The volatile keyword alerts the compiler that 
     					// multiple threads will access the _shouldStop data member, 
     					// and therefore it should not make any optimization 
     					// assumptions about the state of this member

     private string serviceName = ConfigurationManager.AppSettings["ServiceInstanceName"].ToString();
     
    public override void RequestStop()
    {
        _shouldStop = true;
    }
    
    public void Process(object service)
    {
        ServiceInstance = (OMSIntegration)service;
            
        while (ServiceInstance.IsServiceRunning)
        {
            //custom logic here for processing goes here
            .....
            .......
            ......
            
            if (!_shouldStop) 	//check to verify that Stop was not issued - i.e. don't Sleep 
				// if Stop is issued
            {
                logger.Debug("SyncWorker.Process: 
                	Sleeping for the specified interval before resuming polling.");
                Thread.Sleep(pollingWaitSeconds);
            }
        }
    }
}

//Web.config entries
<appSettings>
    <add key="ServicePauseIntervalInSeconds" value="60" />  
    <add key="ServiceAdditionalTimeInSecs" value="5" />
    <add key="ServiceInstanceName" value="OMSIntegrationService" />
      
    <add key="SyncWorkerWaitSeconds" value="1" />
    <add key="SyncPollingFailureWaitMinutes" value="15" />
    <add key="SyncRetryLimit" value="5" />
    <add key="SyncRetryDelayMinutes" value="2" />
    <add key="SyncThreadPoolSize" value="16" />
    <add key="SyncItemsBatchSize" value="200" />    
   
    <add key="WorkflowWorkerWaitSeconds" value="1" />
    <add key="WorkflowWorkerPollingFailureWaitMinutes" value="15" />
    <add key="WorkflowWorkerRetryLimit" value="5" />
    <add key="WorkflowRetryDelaySeconds" value="5" />
    <add key="WorkflowWorkerThreadPoolSize" value="1" />
    <add key="WorkflowWorkerItemsBatchSize" value="10" />    
</appSettings>

Points of Interest

As soon as the Service "Stop" is issued, calls JoinThreads method, that finds out whether the relevant thread is running or not, if running requests additional time of 5 secs (configured in web config) for additional processing of already picked up items and then stops. By implementing this code, once you manually stop the Windows Service, it will take some time to finish the processing and then stop, as opposed to stopping immediately without this code.

One important note though, one cannot have an "Additional Time" of more than 2 minutes - I have read it somewhere - I will post the link if I happen to find it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)