Gracefully Terminating Threads





5.00/5 (3 votes)
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
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.