Introduction
On occasion, I have had the need to spawn off background tasks while leaving the UI responsive. There are several ways to do this. This particular example is suited to .NET 4.0's Task Parallel Library.
Background
Imagine having a program that has to load a large file, parse its contents, and update a UI. You would want the UI to be responsive, and let the loading take place in the background. There are, of course, many other examples of when you just want the program to go off by itself and do stuff and let the user do other stuff in peace and quiet, not sit and wait on an hourglass cursor or a "Please Wait" popup.
Using the Code
The code in the project is well-commented, and the easiest way to understand it is to open it in the IDE. This text is but a brief overview.
Threaded Class HTTPMgr
The worker class, HTTPMgr
, is an example of how to execute multiple tasks concurrently, allowing the user to have a responsive UI.
Class Level Declarations
using System.Threading;
using System.Threading.Tasks;
private CancellationTokenSource m_CancelTokenSource = null;
private List<Task> m_TaskList = null;
m_CancelTokenSource
is made class-level so it can be used to cancel running tasks if desired. Each Task
instance has reference kept in m_TaskList
so they can be acted upon outside the FetchPages()
method.
Core of the FetchPages() Method
m_CancelTokenSource = new CancellationTokenSource();
CancellationToken ReadFileCancelToken = m_CancelTokenSource.Token;
for (Int32 OpIterator = 0; OpIterator < m_HostsToCheck.Count; OpIterator++)
{
Int32 InnerIterator = OpIterator;
Task NewTask = Task.Factory.StartNew(() =>
{
ReadFileCancelToken.ThrowIfCancellationRequested();
try
{
Int32 ByteCounter = 0;
do
{
if (ReadFileCancelToken.IsCancellationRequested)
{
ReadFileCancelToken.ThrowIfCancellationRequested();
}
}
while (ByteCounter > 0);
RaiseOnChange(ReturnStatus.InformationOnly, InnerIterator, PageData, PageInfo);
}
catch (OperationCanceledException exCancel)
{
RaiseOnChange(ReturnStatus.Failure, InnerIterator, "", null);
}
catch (Exception exUnhandled)
{
Shared.WriteToDebugFile(exUnhandled);
}
finally
{
}
}, ReadFileCancelToken);
m_TaskList.Add(NewTask);
}
How To Cancel Running Tasks
public void CancelRunningTasks()
{
try
{
if ((m_CancelTokenSource != null) && (m_TaskList != null))
{
if (m_TaskList.Count > 0)
{
Shared.WriteToDebugFile("Cancelling tasks.");
m_CancelTokenSource.Cancel();
Task.WaitAll(m_TaskList.ToArray<Task>());
}
else
{
Shared.WriteToDebugFile("Cancel tasks requested, but no tasks to cancel.");
}
}
else
{
Shared.WriteToDebugFile("Cancel tasks requested,
but no token to use or no task list was found.");
}
}
catch (Exception ex)
{
Shared.WriteToDebugFile(ex);
}
finally
{
}
}
How Many Tasks Are Remaining?
public Int32 TasksRemaining(out Int32 TasksCompleted,
out Int32 TasksCanceled,
out Int32 TasksFaulted,
out Int32 TasksInWaiting)
{
DateTime dtmMethodStart = DateTime.Now;
Int32 NumTasksRemaining = 0;
TasksCompleted = 0;
TasksCanceled = 0;
TasksFaulted = 0;
TasksInWaiting = 0;
try
{
if ((m_CancelTokenSource != null) && (m_TaskList != null))
{
if (m_TaskList.Count > 0)
{
foreach (Task ATask in m_TaskList)
{
if (ATask != null)
{
if (ATask.IsCompleted)
{
TasksCompleted += 1;
}
if (ATask.IsCanceled)
{
TasksCanceled += 1;
}
if (ATask.IsFaulted)
{
TasksFaulted += 1;
}
if ((ATask.Status == TaskStatus.Running) ||
(ATask.Status == TaskStatus.WaitingForActivation) ||
(ATask.Status == TaskStatus.WaitingForChildrenToComplete) ||
(ATask.Status == TaskStatus.WaitingToRun))
{
NumTasksRemaining += 1;
}
if ((ATask.Status == TaskStatus.WaitingForActivation) ||
(ATask.Status == TaskStatus.WaitingForChildrenToComplete) ||
(ATask.Status == TaskStatus.WaitingToRun))
{
TasksInWaiting += 1;
}
}
}
}
}
}
catch (Exception ex)
{
Shared.WriteToDebugFile(ex);
}
finally
{
}
return NumTasksRemaining;
}
Consuming the Threaded Class (MainForm.cs)
The UI, in this case MainForm.cs, is what instantiates HTTPMgr
and uses it. HTTPMgr
raises events back to MainForm.cs to indicate status, and MainForm.cs uses a timer to check how many tasks are still remaining. While the HTTPMgr
instance is running, the form remains usable since the tasks are executing in the background.
One might ask if there is an issue with raising an event in one thread back to the UI thread? If you will notice in the methods delegated to the event in MainForm.cs, "InvokeRequired
" and "BeginInvoke
" are used to make sure one thread does not cross over into another.
MainForm.cs Declaration
HTTPMgr m_WorkerObject = null;
Starting Up the HTTPMgr Object
This code instantiates the object, wires up the event to the local method in the form using the delegate, and starts the method to do something in the background (in this case, load a bunch a web pages).
m_WorkerObject = new HTTPMgr();
m_WorkerObject.OnChange += new HTTPMgr.ChangeDelegate(WorkerObject_OnChange);
m_WorkerObject.FetchPages();
Receiving Information from the Threaded Operations
This code shows how we transfer information from a thread while it is in operation. You could also have the thread(s)/tasks(s) operate on an object you get back later.
private void WorkerObject_OnChange(HTTPMgr.ReturnStatus MsgType,
Int32 Index, String Message, List<String> AdditionalInfo)
{
if (this.InvokeRequired)
{
this.BeginInvoke(new HTTPMgr.ChangeDelegate(WorkerObject_OnChange),
MsgType, Index, Message, AdditionalInfo);
}
else
{
try
{
}
catch (Exception ex)
{
Shared.WriteToDebugFile(ex);
}
}
}
Checking the Status of the Asynchronous HTTPMgr FetchPages() Process
To keep an eye on the progress, I used a timer to get the task status from the HTTPMgr
instance.
private void PageTimer_Tick(object sender, EventArgs e)
{
try
{
if (m_WorkerObject != null)
{
Int32 TasksCompleted = 0;
Int32 TasksCanceled = 0;
Int32 TasksFaulted = 0;
Int32 TasksInWaiting = 0;
Int32 TasksRemaining = 0;
Int32 TasksRunning = 0;
TasksRemaining = m_WorkerObject.TasksRemaining(out TasksCompleted,
out TasksCanceled,
out TasksFaulted,
out TasksInWaiting);
TasksRunning = TasksRemaining - TasksInWaiting;
TaskRunningLabel.Text = String.Format
(TaskRunningLabel.Tag.ToString(), TasksRunning.ToString());
TasksCanceledLabel.Text = String.Format
(TasksCanceledLabel.Tag.ToString(), TasksCanceled.ToString());
TasksCompletedLabel.Text = String.Format
(TasksCompletedLabel.Tag.ToString(), TasksCompleted.ToString());
TasksFaultedLabel.Text = String.Format
(TasksFaultedLabel.Tag.ToString(), TasksFaulted.ToString());
TasksWaitingLabel.Text = String.Format
(TasksWaitingLabel.Tag.ToString(), TasksInWaiting.ToString());
if (StartButton.Text != "Stopping")
{
if (TasksRemaining == 0)
{
StartButton.Text = "Start";
StartButton.Enabled = true;
PageTimer.Enabled = false;
m_WorkerObject.OnChange -= WorkerObject_OnChange;
m_WorkerObject.Dispose();
m_WorkerObject = null;
}
}
}
}
catch (Exception ex)
{
Shared.WriteToDebugFile(ex);
}
}
Well, that is about it. Any functionality that ties up your UI and could be run in the background can be handled this way. The more responsive the app is to the user, the more they like it and the more professional you look.
Points of Interest
Users generally seem amazed that they can do things with the UI and it is not frozen while waiting on something long running to finish.
History
- 23rd April, 2014: Initial version