I have a console application running in an app domain. The app domain is started by a custom windows service. The application uses parent tasks to start several child tasks that do work. There can be many parent tasks with children running at any given time as the timer looks for new work.
The handle to all parent tasks is in a List of tasks:
static List<Task> _Tasks = new List<Task>();
The windows service is able to send a stop signal to the running application by putting a string token in an app domain slot when an admin changes an xml config file (or when the calling service is shut down). The application is running on a timer and checks for a signal to shut down in the slot, then attempts to gracefully conclude what it is doing by waiting for all tasks to end.
Tasks are started like so:
Task parent = Task.Factory.StartNew(() =>
{
foreach (var invoiceList in exportBucket)
{
KeyValuePair<string, List<InvoiceInfo>> invoices = new KeyValuePair<string, List<InvoiceInfo>>();
invoices = invoiceList;
string taskName = invoices.Key; //file name of input file
Task<bool> task = Task.Factory.StartNew<bool>(state => ExportDriver(invoices),
taskName, TaskCreationOptions.AttachedToParent);
}
});
_Tasks.Add(parent);
A custom GAC dll holds a class that does the work. There are no shared objects in the GAC function. The GAC class is instantiated in each child task:
Export export = new Export();
Each child task calls a method at some point during execution:
foreach (var searchResultList in SearchResults)
{
foreach (var item in searchResultList)
{
if (item.Documents.Count > 0)
{
//TODO: this is where we get thread issue if telling service to stop
var exported = export.Execute(searchResultList);
totalDocuments += exported.ExportedDocuments.Count();
}
}
}
searchResultList
is not shared between tasks. While the application runs, export.Execute performs as expected for all child tasks. When the stop signal is detected in the application, it attempts to wait for all child tasks to end. I've tried a couple ways to wait for the child tasks under each parent to end:
foreach (var task in _Tasks){task.Wait();}
and
while (_Tasks.Count(t => t.IsCompleted) != _Tasks.Count){}
While the wait code executes a threading exception occurs:
Error in Export.Execute() method: System.Threading.ThreadAbortException: Thead was being aborted at WFT.CommonExport.Export.Execute(ICollection '1 searchResults)
I do not wish to use a cancellation token as I want the tasks to complete, not cancel.
I am unclear why the GAC class method is unhappy since each task should have a unique handle to the method object.
UPDATE:
Thanks for the comments. Just to add further clarification to what was going on here...
In theory, there shouldn't be any reason the approach of waiting on child tasks:
while (_Tasks.Count(t => t.IsCompleted) != _Tasks.Count){}
shouldn't work, though
Task.WaitAll()
is certainly a better approach, and helped flesh out the problem. After further testing it turns out that one of the issues was that when the app domain application told the calling service no work was being done by populating a slot read by the service:
AppDomain.CurrentDomain.SetData("Status", "Not Exporting");
the timing and placement of that statement in code was wrong in the application. Being new to multithreading, it took me a while to figure out tasks were still running when I issued SetData
to "Not Exporting". And so, when the service thought it was OK to shut down and tore down the app domain, I believe that caused the ThreadAbortException
. I've since moved the SetData
statement to a more reliable location.
ThreadAbortException is a special exception that can be caught, but it will automatically be raised again at the end of the catch block. When this exception is raised, the runtime executes all the finally blocks before ending the thread.
If an application is idle for some time (meaning that no requests are coming in), or certain other conditions are met, ASP.NET will recycle the entire AppDomain . When that happens, any threads that you started from that AppDomain , including those from Application_Start , will be aborted.
When this method is invoked on a thread, the system throws a ThreadAbortException in the thread to abort it. ThreadAbortException is a special exception that can be caught by application code, but is re-thrown at the end of the catch block unless ResetAbort is called.
To answer your question: you are receiving a thread-abort because tasks are executed on a "background" thread.
Background threads are not waited-on before application terminating. See this MSDN link for further explanation.
Now to try to help solve your actual problem, I would suggest the Task.WaitAll()
method Jim mentions, however you should handle application termination more robustly. I suspect that while you wait for tasks to complete before shutting down, you don't prevent new tasks from being enqueued.
I would recommend an exit-blocking semaphore and the system that enques tasks increment this on initialization, and decrement on dispose.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With