(Print this page)

System.Threading.Task class
Published date: Wednesday, October 30, 2013
On: Moer and Éric Moreau's web site

Introduced with the .Net Framework 4.0 (Visual Studio 2010), this namespace is yet another way of enabling your applications to do multiple things at a single time if your computer has multiple cores.

Developers have been trying hard in the last years to produce applications that are non-blocking, that react quickly to users, that are cancelable for long processes, … and it wasn’t always easy. System.Threading.Threads, Async calls, and BackgroundWorker component were good but not as good as this newer iteration.

System.Threading.Task is the next evolution trying to better accomplish this task. If you think of building Windows Store applications, this is a must use.

If all goes as expected, the article of next month should be about a similar paradigm: Async and Await.

Requirements

To be able to use the System.Threading.Task class, you need to target the .Net Framework 4.0 which was introduced with Visual Studio 2010.

The downloadable demo

This month downloadable demo is a solution created with Visual Studio 2013 containing both a VB and a C# project.

The demo project is doing some random calculation in a loop so we have some time to test the UI while the computation is processing.

Figure 1: picture of running application

The way it has always been

Take a look at this code (btnStart_Click). You see all usual stuff. Changing the value of a label, calling a method that processes some fancy calculation, output results, …

Dim initial As Double = 30
Dim exercise As Double = 30
Dim up As Double = 1.4
Dim down As Double = 0.8
Dim interest As Double = 1.08
Dim periods As Long = 30
Dim sims As Long = 5000000

Dim count As Integer = Convert.ToInt32(lblCount.Text)
count += 1
lblCount.Text = count.ToString(CultureInfo.InvariantCulture)

'Run simulation to price option:
Dim rand As Random = New Random()
Dim start As Integer = Environment.TickCount

'NOTE: the cancellation token passed here is only because the Simulation method is expecting it
'it won't be used in this first scenario
Dim price As Double = Simulation(m_cts.Token, rand, initial, exercise, up, down, interest, periods, sims)

Dim [stop] As Integer = Environment.TickCount

Dim elapsedTimeInSecs As Double = ([stop] - start) / 1000.0

Dim result As String = String.Format("{0:C}  [{1:#,##0.00} secs]", price, elapsedTimeInSecs)

'Display the results once computation is done
ListBox1.Items.Insert(0, result)

'update rest of UI:
count = Convert.ToInt32(lblCount.Text)
count -= 1
lblCount.Text = count.ToString(CultureInfo.InvariantCulture)

If you run the demo and click the “Start without Task” button (the one from the left), you shouldn’t see the label being updated (even if we have code specifically written for that), the window is not resizable, not movable, … in short, not a good user experience. Raise your hand if you have some code like this. The simulation should run in 3-6 seconds depending on your configuration.

The reason of this unresponsive application is the call to the Simulation method. From all that code, this is where the time is spent. This is what we have to transform.

“Taskifying” this process

We will start from the previous code and try to modify it to something more responsive/user-friendly using the Task class. Most of the code will be the same.

We will need to surround the long-running code in a task. But that’s not all. The Simulation method is calculating a value. Because the Simulation will run in a thread other than the UI thread, we won’t be able to access the controls on the form from that thread. We will need to pass back the value from the thread to a second one that will have access to user interface. Enough talking (or writing), let’s see some code!

The first part of the second click event handler (btnStartTask_Click) is identical to the previous one:

Dim initial As Double = 30
Dim exercise As Double = 30
Dim up As Double = 1.4
Dim down As Double = 0.8
Dim interest As Double = 1.08
Dim periods As Long = 30
Dim sims As Long = 5000000

Dim count As Integer = Convert.ToInt32(lblCount.Text)
count += 1
lblCount.Text = count.ToString(CultureInfo.InvariantCulture)

Then 2 lines that will explore in the next section:

btnCancel.Enabled = True
Dim token As CancellationToken = m_cts.Token

Then comes the task definition itself. It is declared using a lambda expression

' Run simulation on a separate task to price option:
Dim T As Task(Of String) = New Task(Of String)(
                              Function() As String
                                  'Run simulation to price option:
                                  Dim rand As Random = New Random()
                                  Dim start As Integer = Environment.TickCount

                                  'NOTE: we pass cancellation token to simulation code so it can check for
                                  'cancellation and respond accordingly.
                                  Dim price As Double = Simulation(token, rand, initial, exercise, up, down, interest, periods, sims)

                                  Dim [stop] As Integer = Environment.TickCount

                                  Dim elapsedTimeInSecs As Double = ([stop] - start) / 1000.0

                                  Dim result As String = String.Format("{0:C}  [{1:#,##0.00} secs]", price, elapsedTimeInSecs)

                                  Return result
                              End Function, token)

If you forget the task declaration, do you recognize the code? It is the same as the previous one. The only exception here is that we do not display the result into the listbox control, we return it to the main thread.

Now that we have declare our thread, we need to launch it. And before running it, because we will want to disabled the Cancel button when there will be no more running task, we need to keep a trace of launched task:

'add task to list so we can cancel computation if necessary:
m_running.Add(T)
'and start it running:
T.Start()

The task will now start running and the UI will remain responsive. But we haven’t done any to display the result yet. We cannot just add a line of code after having called the Start method. Chances are that that line would be executed before the task completes. This is not what we want. We want the task to complete and then display the result.

This is why we will use the ContinueWith method of the Task class. The lambda will be executed only once the task has completed (successfully or not). This is the code:

' Display the results once computation task is done, but ensure
' task is run on UI's thread context so UI update is legal:
T.ContinueWith(
    Sub(antecedent)
        Dim result As String

        ' computation task has finished, remove from cancellation list:
        m_running.Remove(antecedent)

        ' Properly handle exceptions from computation task, e.g. possible cancellation:
        Try
            result = antecedent.Result
        Catch ae As AggregateException
            If TypeOf ae.InnerException Is OperationCanceledException Then
                result = "<<canceled>>"
            Else
                result = String.Format("<<error: {0}>>", ae.InnerException.Message)
            End If
        Catch ex As Exception
            result = String.Format("<<error: {0}>>", ex.Message)
        End Try

        ListBox1.Items.Insert(0, result)

        ' update rest of UI:
        count = Convert.ToInt32(lblCount.Text)
        count -= 1
        lblCount.Text = count.ToString(CultureInfo.InvariantCulture)

        If count = 0 Then
            btnCancel.Enabled = False
        End If

        ' must run on UI thread since updating GUI:
    End Sub, TaskScheduler.FromCurrentSynchronizationContext())

The antecedent argument will receive a pointer to the task that has just completed. Then the task that has just completed is removed from the list of active tasks. We are now ready to try to access the result of the Simulation method returned by the previous task. For that purpose, we can access our antecedent argument and its Result property. I said try to access because nothing guarantees that the task completed successfully. This is why the Result property is encapsulated into a try-catch. The listbox can then show the result (or the error). The counter is decremented to tell the user how many more simulations/tasks are running. Finally, when all the cancel button is disabled when no more task are running.

The last line of the last code snippet shows a really important parameter to the ContinueWith method. It says “FromCurrentSynchronizationContext”. Without this argument, this ContinueWith would run in a thread that is not the same as the UI. By passing this value, we force the ContinueWith to run in the same context where it was launched. The context in this example is the click event. Because this event is triggered by the user when he clicks the button, we are sure that we are on the UI thread and thus have no problem accessing the controls.

Let’s go, try the application again but this time use the right button (Start with Task). Click it 5 or 6 times. You will see the counter incrementing. Try to move and resize the form (it should work). At some point, you should see some results appearing in the listbox.

Isn’t it better?

Cancelling task

Now that we have the application running smoothly by using tasks, it would be nice to be able to cancel the currently running tasks. Remember that we already have passed a token when we started the task.

We need to do a couple of things to support the cancellation of running tasks.

First we need the token. I have declared (m_cts) it at the class level because it appears at a couple of places in the various methods. It is also initialized in the form’s load event (constructor in C#).

Private m_cts As CancellationTokenSource

Then we need a way to let the user cancel the running tasks. In my demo, it is a button. The code in the click event is simple:

'tell running tasks to cancel:
m_cts.Cancel()
'after tasks are cancelled, create new token source for future tasks:
m_cts = New CancellationTokenSource()

You might wonder why we re-instantiate the instance in this event handler. If you cancel the running task and restart a new one, because the last status was canceled, the new one will automatically be canceled. You don’t believe me? Just comment out this line and try by yourself.

If you remember, when we have started the task, we passed the current Token to it. When our task will run, it will be able to inspect its state.

In the cancel button’s click event, we called the Cancel method but this is not enough to stop the task. The code of the task needs to regularly check the state of the token and react accordingly. The Simulation method is already in a loop. So it is easy to react. You will find that line:

ct.ThrowIfCancellationRequested()

If the Cancel button is clicked, the Cancel method of the token is called. The next time this line will run, an exception of type OperationCanceledException will be thrown. Because the method has no exception handler, it will automatically be bubbled to the caller.

The last thing we need to do is to handle it properly in the ContinueWith. This is why the try-catch is important.

You might find it cumbersome to test the cancellation button in your app if you only hit F5 (Start Debugging) to launch it because the debugger will immediately stop when the exception is thrown. To test this part of your application, you should really hit CTRL-F5 (Start Without Debugging).

Test it again. Now we have something awesome!

This is just the tip of the iceberg!

There is a lot more when using tasks. But I really think you have a good start here.

If you have a Pluralsight subscription, Dr. Joe Hummel is offering a very interesting course titled “Introduction to Async and Parallel Programming in .NET 4”.

Conclusion

It requires more work on your side but it is really worth it. Your users will love it.

Next month, if everything rolls as I expect, I will continue with a related subject: Async and Await introduced with the .Net Framework 4.5 (Visual Studio 2012).


(Print this page)