Introducing the Task Parallel Library in Microsoft Visual C# 2010
- 4/15/2010
- Why Perform Multitasking by Using Parallel Processing?
- Implementing Multitasking in a Desktop Application
- Using Tasks and User Interface Threads Together
- Canceling Tasks and Handling Exceptions
- Chapter 27 Quick Reference
Using Tasks and User Interface Threads Together
The section "Why Perform Multitasking by Using Parallel Processing?" at the start of this chapter highlighted the two principal reasons for using multitasking in an application—to improve throughput and increase responsiveness. The TPL can certainly assist in improving throughput, but you need to be aware that using the TPL alone is not the complete solution to improving responsiveness, especially in an application that provides a graphical user interface. In the GraphDemo application used as the basis for the exercises in this chapter, although the time taken to generate the data for the graph is reduced by the effective use of tasks, the application itself exhibits the classic symptoms of many GUIs that perform processor-intensive computations—it is not responsive to user input while these computations are being performed. For example, if you run the GraphDemo application from the previous exercise, click Plot Graph, and then try and move the Graph Demo window by clicking and dragging the title bar, you will find that it does not move until after the various tasks used to generate the graph have completed and the graph is displayed.
In a professional application, you should ensure that users can still use your application even if parts of it are busy performing other tasks. This is where you need to use threads as well as tasks.
In Chapter 23, you saw how the items that constitute the graphical user interface in a WPF application all run on the same user interface (UI) thread. This is to ensure consistency and safety, and it prevents two or more threads from potentially corrupting the internal data structures used by WPF to render the user interface. Remember also that you can use the WPF Dispatcher object to queue requests for the UI thread, and these requests can update the user interface. The next exercise revisits the Dispatcher object and shows how you can use it to implement a responsive solution in conjunction with tasks that ensure the best available throughput.
Improve responsiveness in the GraphDemo application
Return to Visual Studio 2010, and display the GraphWindow.xaml.cs file in the Code and Text Editor window if it is not already open.
Add a new method called doPlotButtonWork below the plotButton_Click method. This method should take no parameters and not return a result. In the next few steps, you will move the code that creates and runs the tasks that generate the data for the graph to this method, and you will run this method on a separate thread, leaving the UI thread free to manage user input.
private void doPlotButtonWork() { }
Move all the code except for the if statement that creates the graphBitmap object from the plotButton_Click method to the doPlotButtonWork method. Note that some of these statements attempt to access user interface items; you will modify these statements to use the Dispatcher object later in this exercise. The plotButton_Click and doPlotButtonWork methods should look like this:
private void plotButton_Click(object sender, RoutedEventArgs e) { if (graphBitmap == null) { graphBitmap = new WriteableBitmap(pixelWidth, pixelHeight, dpiX, dpiY, PixelFormats.Gray8, null); } } private void doPlotButtonWork() { int bytesPerPixel = (graphBitmap.Format.BitsPerPixel + 7) / 8; int stride = bytesPerPixel * pixelWidth; int dataSize = stride * pixelHeight; Stopwatch watch = Stopwatch.StartNew(); Task<byte[]> getDataTask = Task<byte[]>.Factory.StartNew(() => getDataForGraph(dataSize)); byte[] data = getDataTask.Result; duration.Content = string.Format("Duration (ms): {0}", watch.ElapsedMilliseconds); graphBitmap.WritePixels(new Int32Rect(0, 0, pixelWidth, pixelHeight), data, stride, 0); graphImage.Source = graphBitmap; }
In the plotButton_Click method, after the if block, create an Action delegate called doPlotButtonWorkAction that references the doPlotButtonWork method, as shown here in bold:
private void plotButton_Click(object sender, RoutedEventArgs e) { ... Action doPlotButtonWorkAction = new Action(doPlotButtonWork); }
Call the BeginInvoke method on the doPlotButtonWorkAction delegate. The BeginInvoke method of the Action type executes the method associated with the delegate (in this case, the doPlotButtonWork method) on a new thread.
The BeginInvoke method takes parameters you can use to arrange notification when the method finishes, as well as any data to pass to the delegated method. In this example, you do not need to be notified when the method completes and the method does not take any parameters, so specify a null value for these parameters as shown in bold here:
private void plotButton_Click(object sender, RoutedEventArgs e) { ... Action doPlotButtonWorkAction = new Action(doPlotButtonWork); doPlotButtonWorkAction.BeginInvoke(null, null); }
The code will compile at this point, but if you try and run it, it will not work correctly when you click Plot Graph. This is because several statements in the doPlotButtonWork method attempt to access user interface items, and this method is not running on the UI thread. You met this issue in Chapter 23, and you also saw the solution at that time—use the Dispatcher object for the UI thread to access UI elements. The following steps amend these statements to use the Dispatcher object to access the user interface items from the correct thread.
Add the following using statement to the list at the top of the file:
using System.Windows.Threading;
The DispatcherPriority enumeration is held in this namespace. You will use this enumeration when you schedule code to run on the UI thread by using the Dispatcher object.
At the start of the doPlotButtonWork method, examine the statement that initializes the bytesPerPixel variable:
private void doPlotButtonWork() { int bytesPerPixel = (graphBitmap.Format.BitsPerPixel + 7) / 8; ... }
This statement references the graphBitmap object, which belongs to the UI thread. You can access this object only from code running on the UI thread. Change this statement to initialize the bytesPerPixel variable to zero, and add a statement to call the Invoke method of the Dispatcher object, as shown in bold here:
private void doPlotButtonWork() { int bytesPerPixel = 0; plotButton.Dispatcher.Invoke(new Action(() => { bytesPerPixel = (graphBitmap.Format.BitsPerPixel + 7) / 8; }), DispatcherPriority.ApplicationIdle); ... }
Recall from Chapter 23 that you can access the Dispatcher object through the Dispatcher property of any UI element. This code uses the plotButton button. The Invoke method expects a delegate and an optional dispatcher priority. In this case, the delegate references a lambda expression. The code in this expression runs on the UI thread. The DispatcherPriority parameter indicates that this statement should run only when the application is idle and there is nothing else more important going on in the user interface (such as the user clicking a button, typing some text, or moving the window).
Examine the final three statements in the doPlotButtonWork method. They look like this:
private void doPlotButtonWork() { ... duration.Content = string.Format("Duration (ms): {0}", watch.ElapsedMilliseconds); graphBitmap.WritePixels(new Int32Rect(0, 0, pixelWidth, pixelHeight), data, stride, 0); graphImage.Source = graphBitmap; }
These statements reference the duration, graphBitmap, and graphImage objects, which are all part of the user interface. Consequently, you must change these statements to run on the UI thread.
Modify these statements, and run them by using the Dispatcher.Invoke method, as shown in bold here:
private void doPlotButtonWork() { ... plotButton.Dispatcher.Invoke(new Action(() => { duration.Content = string.Format("Duration (ms): {0}", watch. ElapsedMilliseconds); graphBitmap.WritePixels(new Int32Rect(0, 0, pixelWidth, pixelHeight), data, stride, 0); graphImage.Source = graphBitmap; }), DispatcherPriority.ApplicationIdle); }
This code converts the statements into a lambda expression wrapped in an Action delegate, and then invokes this delegate by using the Dispatcher object.
On the Debug menu, click Start Without Debugging to build and run the application.
In the Graph Demo window, click Plot Graph and before the graph appears quickly drag the window to another location on the screen. You should find that the window responds immediately and does not wait for the graph to appear first.
Close the Graph Demo window.