Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Simple Task Parallel Library (TPL) Example

0.00/5 (No votes)
23 Apr 2014 1  
Writing a threaded application with a responsive UI

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;
 
/// <summary>
/// The object used for cancelling tasks.
/// </summary>
private CancellationTokenSource m_CancelTokenSource = null;
 
/// <summary>
/// The list of created tasks.
/// </summary>
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

// First we need a source object for the cancellation token.
m_CancelTokenSource = new CancellationTokenSource();
 
// Get a reference to the cancellation token.
CancellationToken ReadFileCancelToken = m_CancelTokenSource.Token;
 
// I am iterating through the list of URLs to use one task per URL to do them in parallel.
for (Int32 OpIterator = 0; OpIterator < m_HostsToCheck.Count; OpIterator++)
{
 
    // Make a new variable for use in the thread since OpIterator is in the main thread.
    // One might think it doesn't make a difference, but it does.
    Int32 InnerIterator = OpIterator;
 
    Task NewTask = Task.Factory.StartNew(() =>
    {
        // If cancel has been chosen, throw an exception now before doing anything.
        ReadFileCancelToken.ThrowIfCancellationRequested();
 
        try
        {
            // ... do some stuff here
 
            Int32 ByteCounter = 0;
 
            do
            {
                // Since we are in as ParallelLoopResult, checked for as "Cancel" again at each
                // iteration of the loop. If cancellation is requested, then make sure 
                // the cancellation exception is thrown.
                if (ReadFileCancelToken.IsCancellationRequested)
                {
                    ReadFileCancelToken.ThrowIfCancellationRequested();
                }
 
                // ... do some other stuff in a loop here.
            }
            while (ByteCounter > 0); // any more data to read?
 
            // Print out page source
            // Call back with the text for the web page and a name. InnerIterator is used
            // in this example to tell the caller which WebBrowser instance to use.
            RaiseOnChange(ReturnStatus.InformationOnly, InnerIterator, PageData, PageInfo);
 
        }  // END try
 
        catch (OperationCanceledException exCancel)
        {
            // This is the exception raised on a Cancel.  It is not really an error,
            // but a planned exception. So we tell the caller.
            RaiseOnChange(ReturnStatus.Failure, InnerIterator, "", null);
 
        }  // END catch (OperationCanceledException exCancel)
        catch (Exception exUnhandled)
        {
            // Debug logging for this example project.
            // Replace it with your logging, if you want.
            Shared.WriteToDebugFile(exUnhandled);
                            
        }  // END catch (Exception exUnhandled)
        finally
        {
            // Clean up resources.
 
        }  // END finally
 
    }, ReadFileCancelToken);  
 
    // The task is now defined and started, 
    // so the object referencing the task is now added to the list.
    m_TaskList.Add(NewTask);
 
}  // END for (int OpIterator = 0; OpIterator < 100; OpIterator++)

How To Cancel Running Tasks

/// <summary>
/// Cancels whatever tasks are running.
/// </summary>
public void CancelRunningTasks()
{ 
    try
    {
        // To cancel tasks, we must have:
        //   1 - An object referencing the cancellation token
        //   2 - A list of tasks
        if ((m_CancelTokenSource != null) && (m_TaskList != null))
        {
            // 3 - And, of course, we need tasks to cancel. :)
            if (m_TaskList.Count > 0)
            {
                // Log it for your convenience.
                Shared.WriteToDebugFile("Cancelling tasks.");
 
                // Since all tasks reference this cancellation token, just tell the
                // token reference to cancel. As each task executes, this is checked.
                m_CancelTokenSource.Cancel();
 
                // Now just wait for the tasks to finish.
                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.");
        }
 
    } // END try
 
    catch (Exception ex)
    {
        // Debug logging for this example project.
        // Replace it with your logging, if you want.
        Shared.WriteToDebugFile(ex);
 
    }  // END catch (Exception ex)
 
    finally
    {
 
    }  // END finally
 
}  // END public void CancelRunningTasks()

How Many Tasks Are Remaining?

/// <summary>
/// Returns the number of tasks left to complete,
/// and by out parameters, other counts of task states.
/// </summary>
/// <param name="TasksCompleted">The # of tasks that are run and done.</param>
/// <param name="TasksCanceled">The # of tasks that were cancelled.</param>
/// <param name="TasksFaulted">The # of tasks that reported a faulted state.</param>
/// <param name="TasksInWaiting">The # of tasks in some waiting state.</param>
/// <returns>The # of tasks either running or waiting to run.</returns>
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;
                        }  // END if (ATask.IsCompleted)
 
                        if (ATask.IsCanceled)
                        {
                            TasksCanceled += 1;
                        }  // END if (ATask.IsCompleted)
 
                        if (ATask.IsFaulted)
                        {
                            TasksFaulted += 1;
                        }  // END if (ATask.IsFaulted)
 
                        if ((ATask.Status == TaskStatus.Running) ||
                            (ATask.Status == TaskStatus.WaitingForActivation) ||
                            (ATask.Status == TaskStatus.WaitingForChildrenToComplete) ||
                            (ATask.Status == TaskStatus.WaitingToRun))
                        {
                            NumTasksRemaining += 1;
                        }  // END if ((ATask.Status == TaskStatus.Running) || 
                           // (ATask.Status == TaskStatus.WaitingForActivation) || 
                           // (ATask.Status == TaskStatus.WaitingForChildrenToComplete) || 
                           // (ATask.Status == TaskStatus.WaitingToRun))
 
                        if ((ATask.Status == TaskStatus.WaitingForActivation) ||
                            (ATask.Status == TaskStatus.WaitingForChildrenToComplete) ||
                            (ATask.Status == TaskStatus.WaitingToRun))
                        {
                            TasksInWaiting += 1;
                        }  // END if ((ATask.Status == TaskStatus.WaitingForActivation) || 
                           // (ATask.Status == TaskStatus.WaitingForChildrenToComplete) || 
                           // (ATask.Status == TaskStatus.WaitingToRun))
 
                    }  // END if (ATask != null)
 
                }  // END foreach (Task ATask in m_TaskList)
 
            }  // END if (m_TaskList.Count > 0)
 
        }  // END if ((m_CancelTokenSource != null) && (m_TaskList != null))
 
    } // END try
 
    catch (Exception ex)
    {
        // Debug logging for this example project.
        // Replace it with your logging, if you want.
        Shared.WriteToDebugFile(ex);
 
    }  // END catch (Exception exUnhandled)
 
    finally
    {
 
    }  // END finally
 
    return NumTasksRemaining;
 
}  // END public Int32 TasksRemaining(out Int32 TasksCompleted, 
   // out Int32 TasksCanceled, out Int32 TasksFaulted, out Int32 TasksInWaiting)

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

/// <summary>
/// This is the object that handles the HTTP page loading.  It is module-level so that
/// the callback can be handled outside the calling method.
/// </summary>
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.

/// <summary>
/// This is the method called by the callback of whatever task is trying 
/// to pass back information.
/// A check is made so that the callback thread does not try to access objects 
/// that exist in the UI thread.
/// </summary>
/// <param name="MsgType"></param>
/// <param name="Index"></param>
/// <param name="Message"></param>
/// <param name="AdditionalInfo"></param>
private void WorkerObject_OnChange(HTTPMgr.ReturnStatus MsgType, 
        Int32 Index, String Message, List<String> AdditionalInfo)
{ 
    if (this.InvokeRequired)
    {
        // This callback is in the caller's thread, which is not the UI thread. 
        // So we tell the form to call this method using the data provided in the UI thread. 
        // The call goes on a stack, and is executed in the UI thread.
        this.BeginInvoke(new HTTPMgr.ChangeDelegate(WorkerObject_OnChange), 
                         MsgType, Index, Message, AdditionalInfo);
    }
    else
    {
        // If we are here, we are in the UI thread.
        try
        {
        // ... do some UI stuff.
        }  // END try
        catch (Exception ex)
        {
            // Debug logging for this example project.
            // Replace it with your logging, if you want.
            Shared.WriteToDebugFile(ex);
 
        }
    }  // END else of [if (this.InvokeRequired)]
 
}  // END private void WorkerObject_OnChange(HTTPMgr.ReturnStatus MsgType, 
   // String Message, Dictionary<String, Object> AdditionalInfo)

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.

/// <summary>
/// The timer is used to monitor the status of the tasks.  
/// Once there are no tasks remaining, the Start button is restored to showing Start.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PageTimer_Tick(object sender, EventArgs e)
{ 
    try
    {
        if (m_WorkerObject != null)
        {
            // The method requires these as out parameters, but they are not used here.  
            // They were put in the method for illustrative purposes, 
            // not that they are necessary.
            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 Cancelling (button shows Stopping), we do not need to do anything.
            // Most likely, this event is not going to fire under that condition.
            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;
                }  // END if (m_WorkerObject.TasksRemaining
                   // (out TasksCompleted, out TasksCanceled, 
                   // out TasksFaulted, out TasksInWaiting) == 0)
 
            }  // END if (StartButton.Text != "Stopping")
 
        }  // END if (m_WorkerObject != null)
 
    }  // END try
    catch (Exception ex)
    {
        // Debug logging for this example project.
        // Replace it with your logging, if you want.
        Shared.WriteToDebugFile(ex);
 
    }
 
}  // END private void PageTimer_Tick(object sender, EventArgs e)

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here