Long-Running Operations


Imagine that the value of pi in System.Math.PI, at only 20 digits, just isn't precise enough for you. In that case, you may find yourself writing an application like the one in Figure 18.1 to calculate pi to an arbitrary number of digits.

Figure 18.1. Digits of Pi Application


This program takes as input the number of digits of pi to calculate and, when the Calculate button is pressed, shows the progress as the calculation happens.

Progress Indication

Although some applications don't need to calculate the digits of pi,many kinds of applications need to perform long-running operations, whether it's printing, making a web service call, or calculating the interest earnings of a certain multibillionaire in the Pacific Northwest. Users are generally content to wait for such things as long as they can see that progress is being made. That's why even our simple pi application has a progress bar.

The algorithm to calculate pi calculates 9 digits at a time. As each new set of digits is available, the application updates the text and the progress bar. For example, Figure 18.2 shows progress on the way to calculating 1,000 digits of pi (if 21 digits are good, then 1,000 must be better).

Figure 18.2. Calculating Pi to 1,000 Digits


The following shows how the UI is updated as the digits of pi are calculated:

// SyncCalcPiForm.cs partial class SyncCalcPiForm : Form {   ...   void ShowProgress(string pi, int totalDigits, int digitsSoFar) {     // Display progress in UI     this.resultsTextBox.Text = pi;     this.calcToolStripProgressBar.Maximum = totalDigits;     this.calcToolStripProgressBar.Value = digitsSoFar;    if( digitsSoFar == totalDigits ) {      // Reset progress UI      this.calcToolStripStatusLabel.Text = "Ready";      this.calcToolStripProgressBar.Visible = false;   }   // Force UI update to reflect calculation progress   this.Refresh(); }    void CalcPi(int digits) {      StringBuilder pi = new StringBuilder("3", digits + 2);     // Show initial progress     ShowProgress(pi.ToString(), digits, 0);     if( digits > 0 ) {        pi.Append(".");        for( int i = 0; i < digits; i += 9 ) {          int nineDigits = NineDigitsOfPi.StartingAt(i + 1);          int digitCount = Math.Min(digits - i, 9);          string ds = string.Format("{0:D9}", nineDigits);          pi.Append(ds.Substring(0, digitCount));       // Show continuing progress       ShowProgress(pi.ToString(), digits, i + digitCount);        }      }    }    void calcButton_Click(object sender, EventArgs e) {      // Set calculating UI      this.calcToolStripProgressBar.Visible = true;      this.calcToolStripStatusLabel.Text = "Calculating...";      // Calculate pi to the number of digits in the up-down control      // (need to convert up-down control's decimal Value to an integer)      CalcPi((int)this.decimalPlacesNumericUpDown.Value);    } }


This implementation works just fine for a small number of digits. But suppose that, in the middle of calculating pi to a large number of digits, the user switches away from the application and then returns, as shown in Figure 18.3.

Figure 18.3. No Paint for You!


The problem is that the application has a single thread of execution (this kind of application is often called a single-threaded application). Consequently, while the thread is calculating pi, it can't also be drawing the UI in response to system paint requests. This didn't happen before the user switched the application to the background, because the call to the form's Refresh method forces an immediate repaint. However, after the user puts the application into the background and then the foreground again, the system requests the main form to repaint its entire client area, and that means processing the Paint event. Because no other event can be processed until the application returns from the Click event on the Calculate button, the user doesn't see any display of progress until all the digits of pi are calculated.

The same problem prevents the client area from processing events related to user input via, for example, the mouse or keyboard. In these situations, if users try repeatedly to click in the results text box or resize the form with the status strip's size grip, the UI locks up and adds the infamous "(Not Responding)" message to the application's title bar, as shown in Figure 18.4.

Figure 18.4. No User Input for You!


The user's only options are either to wait until the Click button event returns or to use the system-managed control box to close the application immediately. To avoid these issues, this application needs a way to free the UI thread to do UI work and handle the long-running pi calculation in the background. For this, it needs another thread of execution.

Asynchronous Operations

A thread of execution (often simply called a thread) is a series of instructions and a call stack that operate independently of the other threads in the application or those in any other application. In every version of Windows since Windows 95, Windows schedules each thread transparently so that a programmer can write a thread almost (but not quite) as if it were the only thing happening on the system.

Starting a thread is an asynchronous operation in that the current thread of execution continues, executing independently of the new thread. In .NET, you start a new thread of execution by creating a Thread object from the System.Threading namespace, passing a delegate as the constructor parameter, and invoking the Start method:[1]

[1] You can read more about delegates in Appendix C: Delegates and Events.

// AsyncCalcPiForm.cs using System.Threading; ... partial class AsyncCalcPiForm  : Form {   ...   void CalcPi(int digits) {...}   void calcButton_Click(object sender, EventArgs e) {     // Set calculating UI     this.calcToolStripProgressBar.Visible = true;     this.calcToolStripStatusLabel.Text = "Calculating...";     // Start pi calculation on new thread of execution     Thread piThread = new Thread(CalcPiThreadStart);     piThread.Start((int)this.decimalPlacesNumericUpDown.Value);   }   void CalcPiThreadStart(object digits) {     // Convert thread start parameter to int     CalcPi((int)digits);   } }


This code creates a new thread and begins execution of the thread when the thread Start method is called. Now, instead of waiting for CalcPi to finish before returning from the button Click event, the UI thread spawns a worker thread before immediately returning to the UI thread and allowing it to continue user interaction duties. Figure 18.5 shows the two threads doing their separate jobs.

Figure 18.5. Naïve Multithreading


Spawning a worker thread to calculate pi leaves the UI thread free to handle events (which Windows Forms creates and fires as it takes messages off the Windows message queue). When the worker thread has more digits of pi to share with the user, it directly sets the values of the text box and the progress bar controls.

Unfortunately, such direct manipulation of controls from the worker thread is a no-no. Luckily, when you execute your application under the debugger, you'll see an Invalid OperationException thrown, as shown in Figure 18.6.[2]

[2] Throwing Invalid OperationException is the default. You can set the static Control.CheckForIllegalCross-ThreadCalls property to false to prevent the InvalidOperationException, although I don't advise it. Also, note that InvalidOperationExceptions are raised only when the app is executing from within the debugger. No exception is raised when your application executes outside a debugger, so you must be vigilant during development.

Figure 18.6. Illegal Cross-Thread Operation Detected and InvalidOperationException Raised


Because we start the CalcPi method on a worker thread, when CalcPi calls the ShowProgress method, ShowProgress accesses the text box and progress bar controls from the worker thread, even though those controls were created on the UI thread. This violates a key requirement that's been present since Windows first got support for threads:

Thou shalt operate on a window only from its creating thread.

In fact, the Windows Forms documentation is clear on this point:

There are four methods on a control that are safe to call from any thread: Invoke, BeginInvoke, EndInvoke, and CreateGraphics. For all other method calls, you should use one of the invoke methods to marshal the call to the control's thread.[3]

[3] See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfSystemWindowsFormsControlClassTopic.asp (http://tinysells.com/42). It is actually possible for CreateGraphics to cause a control to be created on the wrong thread; when CreateGraphics is called from a worker thread on a control that hasn't yet had its HWND created, the act of accessing the control's Handle property (used internally by CreateGraphics) causes the control's HWND to be created. The Graphics object is just fine, but the control's window has been created on the worker thread and cannot be of use from the UI thread. Because accessing the Handle property of a control creates the HWND, you can use that to force its creation on the UI thread. You can also check whether the HWND was created via the IsHandleCreated property from the worker thread.

When the CalcPi method calls the ShowProgress method, it accesses controls created by the UI thread. When the application is executing under the debugger, this causes an InvalidOperationException to be thrown on the first line of the following code:

void ShowProgress(string pi, int totalDigits, int digitsSoFar) {    // Display progress in UI    this.resultsTextBox.Text = pi; // Can't call from worker thread!    ... }


This code is in clear violation.

Safety and Multithreading

Luckily, long-running operations are common in Windows applications. Through the use of custom delegates, applications can take advantage of specific Windows Forms features to ensure that worker threads communicate with the UI thread safely.

Multithreading with Custom Delegates

Recall that the CalcPi application starts a worker thread by passing an object argument to CalcPiThreadStart, the worker thread's entry point:

void calcButton_Click(object sender, EventArgs e) {    ...    // Start pi calculation on new thread of execution    Thread piThread = new Thread(CalcPiThreadStart);    piThread.Start((int)this.decimalPlacesNumericUpDown.Value);  }  void CalcPiThreadStart(object digits) {    // Convert thread start parameter to int    CalcPi((int)digits); }


When the thread start method is called on the worker thread, we simply cast the object parameter to an integer and pass it to the real CalcPi method. Because you can't pass strongly typed arguments to the CalcPiThreadStart method, you might prefer to use custom delegates for spawning threads. Additionally, asynchronously executed delegates are processed on threads allocated from the per-process thread pool, an approach that scales better than creating a new thread for each of a large number of asynchronous operations.

Here's how to declare a custom delegate suitable for calling CalcPi:

delegate void CalcPiDelegate(int digits);


After the custom delegate has been defined, the following code creates an instance of the delegate to call the CalcPi method synchronously:

void calcButton_Click(object sender, EventArgs e) {    ...    // Begin calculating pi synchronously    CalcPiDelegate calcPi = new CalcPiDelegate(CalcPi);    calcPi((int)this.decimalPlacesNumericUpDown.Value); }


Because calling CalcPi synchronously causes our UI to freeze (remember how we got into this discussion in the first place?), we need to call CalcPi asynchronously. Before we do that, however, we need to explain a bit more about how delegates work. The CalcPiDelegate declaration implicitly declares a new class derived from MulticastDelegate (from the System namespace), which has three methods: Invoke, BeginInvoke, and EndInvoke:

namespace System {    ...    class CalcPiDelegate : MulticastDelegate {      public void Invoke(int digits);      public void BeginInvoke(        int digits, AsyncCallback callback, object asyncState);      public void EndInvoke(IAsyncResult result);    }    ... }


When the application created an instance of CalcPiDelegate and called it like a method, it was actually calling the Invoke method, which turned around and synchronously called the CalcPi method on the same thread. BeginInvoke and EndInvoke, however, are the pair of methods that allows asynchronous invocation of a method on a new thread for a per-process pool of threads.

To have the CalcPi method called on another threadthe aforementioned worker threadthe application uses BeginInvoke:

delegate void CalcPiDelegate(int digits); void calcButton_Click(object sender, EventArgs e) {    ...    // Begin calculating pi asynchronously    CalcPiDelegate calcPi = new CalcPiDelegate(CalcPi);    calcPi.BeginInvoke(      (int)this.decimalPlacesNumericUpDown.Value, // CalcPi argument      EndCalcPi, // Called when CalcPi completes      calcPi); // EndCalcPi argument (indirectly) }


When we call BeginInvoke on our CalcPiDelegate, the first argument is always the argument to our CalcPi method. This causes a thread from the thread pool to act as our worker thread, calling CalcPi and then returning the thread to the pool when CalcPi returns. But how will we know when CalcPi has finished executing? In our example, we're getting progress indicators, but what if there's an exception on the worker thread? Also, what if CalcPi returned something other than void? How would we get those results?

To answer these questions, we make sure that the last two arguments to our custom delegate's BeginInvoke method are a delegate to call and an object to pass it when our custom delegate has completed:

void EndCalcPi(IAsyncResult result) {   // Harvest results, handle exceptions, and clean up resources   try {      CalcPiDelegate calcPi = (CalcPiDelegate)result.AsyncState;      calcPi.EndInvoke(result);   }   catch( Exception ex ) {      // EndCalcPi executed on worker thread      ShowProgress(ex.Message, 0, 0); // ERR!   } }


It's certainly possible to write code that never calls EndInvoke, but failing to call EndInvoke causes resources to stick around a lot longer than they should. Also, it's by calling EndInvoke that you can access any results or exceptions from our delegate executing on our worker thread. However, if there is a result or an exception, you should take care not to report it directly to the UI, as I've done here. EndCalcPi is called on a worker thread and not on a UI thread, so you must use the techniques I'm about to show you for that, too.

Detecting UI Access on Worker Threads

At this point, the CalcPi application side of things nicely kicks off the calculation of pi asynchronously, passing in our typed arguments and using a thread from the thread pool. However, the code running on a worker thread is still setting controls created on the UI thread, and that, as you know, is illegal.

Luckily, Windows Forms provides the necessary additional support for long-running operations natively: Each UI class in Windows Formsmeaning every class that ultimately derives from System.Windows.Forms.Controlhas a property that you can use to find out whether it's safe to act on the control from the current thread. The property, InvokeRequired, returns true if the calling thread needs to pass control to the UI thread before calling a method on the control. A simple Assert in the ShowProgress method would have immediately shown the error in our sample application:

using System.Diagnostics;  ...  void ShowProgress(string pi, int totalDigits, int digitsSoFar) {    // Make sure we're on the UI thread    Debug.Assert(this.InvokeRequired == false);    ... }


Because the worker thread is not allowed to show progress directly, we need to pass control from the worker thread back to the UI thread. From the names of the first three methods that are safe to call from any threadInvoke, BeginInvoke, and EndInvokeit should be clear that you need another custom delegate to pass control appropriately. Using the same techniques to create and use the CalcPiDelegate to communicate from the UI thread to the worker thread, we can just as easily create a custom delegate on the worker thread and execute it on the UI thread, giving us safe, single-threaded access to UI objects.

Synchronous Callbacks

Asynchronous operations, such as the call to a delegate's BeginInvoke method, return immediately, so they are nonblocking. This means that the thread isn't blocked waiting for the method to complete. Synchronous operations, on the other hand, are blocking, because they do cause the calling thread to block until the method returns.

Depending on the blocking behavior you're interested in, you can call either Invoke or BeginInvoke on a control when calling into the UI thread:

namespace System.Windows.Forms {    class Control : ... {      public object Invoke(Delegate method);      public virtual object Invoke(Delegate method, object[] args);      public IAsyncResult BeginInvoke(Delegate method);      public virtual IAsyncResult BeginInvoke(        Delegate method, object[] args);      public virtual object EndInvoke(IAsyncResult asyncResult);      ...    } }


Control.Invoke blocks until the UI thread has processed the request. The request is processed by putting a message on the UI thread's message queue and executing the message handler like any other message (in this case, the event handler calls our delegate). Because Invoke takes a Delegate argument, which is the base class for all delegates, it can form a call to any method, using the optional array of objects as arguments and returning an object as the return value for the called method. Using Control.Invoke looks like this:

void ShowProgress(string pi, int totalDigits, int digitsSoFar) {    // Make sure we're on the UI thread    Debug.Assert(this.InvokeRequired == false);    ...    // No need to force UI update when calculating asynchronously    //this.Refresh();  } delegate void ShowProgressDelegate(   string pi, int totalDigits, int digitsSoFar); void CalcPi(int digits) {   StringBuilder pi = new StringBuilder("3", digits + 2);   // Get ready to show progress   ShowProgressDelegate showProgress =     new ShowProgressDelegate(ShowProgress);   // Show initial progress   this.Invoke(     showProgress,   new object[] { pi.ToString(), digits, 0 });    if( digits > 0 ) {      pi.Append(".");      for( int i = 0; i < digits; i += 9 ) {        ...        // Show continuing progress        this.Invoke(          showProgress,          new object[] { pi.ToString(), digits, i + digitCount });      }    } }


Notice the declaration of a new delegate, ShowProgressDelegate. This delegate matches the signature of the ShowProgress method we'd like invoked on the UI thread. Because ShowProgress takes three arguments, the code uses an overload to Invoke that takes an array of objects to form the arguments to the ShowProgress method.

The entire process has the UI thread using a delegate that calls Delegate.BeginInvoke to spawn a worker thread, and the worker thread using Control.Invoke to pass control back to the UI thread when the progress controls need updating. Figure 18.7 shows our safe multithreading architecture.

Figure 18.7. Safe Multithreading


You can see that when the worker thread calls Invoke, the request is placed onto the message queue, thereby allowing the UI thread to retrieve the progress data and safely update the controls appropriately.

Asynchronous Callbacks

Our use of the synchronous call to Control.Invoke works just fine, but it gives us more than we need. The worker thread doesn't get any output or return values from the UI thread when calling through ShowProgressDelegate. By calling Invoke, we force the worker thread to wait for the UI thread, blocking the worker thread from continuing its calculations. This is a job tailor-made for the asynchronous Control.BeginInvoke method:

using System.Threading; ... void CalcPi(int digits) {   StringBuilder pi = new StringBuilder("3", digits + 2);   // Get ready to show progress   ShowProgressDelegate showProgress =     new ShowProgressDelegate(ShowProgress);   // Show initial progress asynchronously   this.BeginInvoke(     showProgress,     new object[] { pi.ToString(), digits, 0 });   if( digits > 0 ) {     pi.Append(".");     for( int i = 0; i < digits; i += 9 ) {       ...       // Show continuing progress asynchronously       this.BeginInvoke(          showProgress,          new object[] { pi.ToString(), digits, i + digitCount });     }   } }


The only difference in this code is the call to BeginInvoke (instead of Invoke) to asynchronously kick off the delegate.

Unlike our custom delegate's BeginInvoke, which we needed to match to a corresponding EndInvoke, there is no call to EndInvoke here. It's true that you should always call a delegate's EndInvoke after a call to a delegate's BeginInvoke, but in this case, we call Control.BeginInvoke, passing a delegate to call on the UI thread. It's completely safe to call Control.BeginInvoke without ever calling Control.EndInvoke, because it doesn't create the same resources associated with a delegate's BeginInvoke call.[4]

[4] Chris Brumme discusses both the need for a call to a delegate's EndInvoke and the optionality of a call to Control.EndInvoke at http://blogs.msdn.com/cbrumme/archive/2003/05/06/51385.aspx (http://tinysells.com/43).

Even if you do want the results from a call to Control.BeginInvoke, there's no way to pass a callback, so you need to use the IAsyncResult implementation as returned from Control.BeginInvoke. You keep checking the IsCompleted property for true during your other worker thread processing before calling Control.EndInvoke to harvest the result. This is such a pain that, if you want results from the call to the UI thread, I suggest that the worker thread use Control.Invoke instead.

Simplified Multithreading

To establish safe, asynchronous, long-running operations with progress reports, you first need to create a delegate for your long-running operation and execute it asynchronously with a call to BeginInvoke, making sure to always call EndInvoke. Second, you use Control.BeginInvoke to update the UI thread from the worker thread with progress visuals. Unfortunately, this technique takes a nontrivial amount of effort.

Fortunately, you can simplify things by using the BackgroundWorker component from the System.ComponentModel namespace. BackgroundWorker builds on the mechanisms I've shown you but gathers them into a component that you can drop onto a form and configure declaratively, as shown in Figure 18.8.

Figure 18.8. The BackgroundWorker Component


This humble component provides the necessary thread communication infrastructure for you, leaving you to configure it and incorporate only the code you need to solve the functional problem at hand.

Initiating a Worker Thread

After you have placed a BackgroundWorker component on your form, you initiate the worker thread by calling BackgroundWorker's RunWorkerAsync method from the Calculate button's Click event handler:

// AsyncCalcPiForm.cs partial class AsyncCalcPiForm : Form {   ...   void calcButton_Click(object sender, EventArgs e) {     ...     // Initiate asynchronous pi calculation on worker thread     this.backgroundWorker.RunWorkerAsync(     (int)this.decimalPlacesNumericUpDown.Value);   } }


RunWorkerAsync instructs BackgroundWorker to create the desired worker thread and begin executing on it. RunWorkerAsync accepts a single object argument, which is dutifully passed to the worker thread and should be used to pass any information that your worker thread code might need. Because the argument is an object, you can pass either a single value, such as the digit's integer value, or a custom type that packages several pieces of information into on object.

Executing from the Worker Thread

BackgroundWorker provides the DoWork event that you handle to process your long-running operation on a worker thread from the thread pool. DoWork is BackgroundWorker's default event, and this means that you double-click BackgroundWorker to have the Windows Forms Designer automatically create a handler and register it:

// AsyncCalcPiForm.Designer.cs partial class AsyncCalcPiForm {   ...   System.ComponentModel.BackgroundWorker backgroundWorker;   ...   void InitializeComponent() {     ...     this.backgroundWorker =     new System.ComponentModel.BackgroundWorker();    ...    // backgroundWorker    this.backgroundWorker.DoWork += this.backgroundWorker_DoWork;     ...    }  }  // AsyncCalcPiForm.cs  partial class AsyncCalcPiForm : Form {    ...    // Executed on a worker thread from the thread pool    void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {      ...    } }


Don't forget that any code that you place within the DoWork event handler is executing from the worker thread and, therefore, must not manipulate controls created on the UI thread. DoWork provides DoWorkEventArgs (from the System.ComponentModel namespace), which, among other things, is the receptacle for data passed from the UI thread to the worker thread. To pull the object passed to RunWorkerAsync, you use the DoWorkEventArgs object's Argument property:

// AsyncCalcPiForm.cs partial class AsyncCalcPiForm : Form {   ...   void calcButton_Click(object sender, EventArgs e) {     ...     // Begin calculating pi asynchronously     this.backgroundWorker.RunWorkerAsync(        (int)this.decimalPlacesNumericUpDown.Value);   }   void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {     CalcPi((int)e.Argument);   } }


When we have acquired the desired number of digits, the rest of the pi calculation can proceed on a worker thread from the pool until completion, at which time BackgroundWorker returns from DoWork and the worker thread is returned to the pool.

Reporting Progress

Our use of BackgroundWorker so far shows how we can rid ourselves of the custom delegate we introduced earlier to start our worker thread but still use a thread from the thread pool. Similarly, BackgroundWorker also provides a simplified communication protocol for reporting progress from the worker thread back to the UI thread. However, the communication infrastructure for reporting progress isn't enabled by default, so you must enable it by setting the BackgroundWorker object's WorkerReportsProgress property to true. For this, you can use the Properties window, as illustrated in Figure 18.9.

Figure 18.9. Enabling BackgroundWorker to Report Progress


Failure to set WorkerReportsProgress to true causes an InvalidOperationException if progress is reported from the worker thread. If the exception is not handled, any exception on the worker thread started by BackgroundWorker causes the application to abruptly hang for no apparent reason (you'll see how to handle worker thread exceptions shortly).

To report progress from the worker thread, you call the BackgroundWorker object's ReportProgress method in one of two ways. First, you can simply pass an integer value that represents the background operation's percentage of completion:

// Report progress percentage this.backgroundWorker.ReportProgress(progressPercentage);


Alternatively, you can use a ReportProgress overload to pass an additional object containing any kind of progress information that needs to be relayed to the UI thread. With regard to calculating pi, we use this technique to bundle the arguments to call ShowProgress:

class AsyncCalcPiForm : Form {    ...    class CalcPiUserState {      public readonly string Pi;      public readonly int TotalDigits;      public readonly int DigitsSoFar;     public CalcPiUserState(        string pi, int totalDigits, int digitsSoFar) {        this.Pi = pi;        this.TotalDigits = totalDigits;        this.DigitsSoFar = digitsSoFar;      }    }    void CalcPi(int digits) {      StringBuilder pi = new StringBuilder("3", digits + 2);     // Report initial progress     this.backgroundWorker.ReportProgress(0,        new CalcPiUserState(pi.ToString(), digits, 0));     if( digits > 0 ) {       pi.Append(".");       for( int i = 0; i < digits; i += 9 ) {         ...         // Report continuing progress         this.backgroundWorker.ReportProgress(0,           new CalcPiUserState(pi.ToString(), digits, i + digitCount));        }      }    } }


In our example, we've updated CalcPi to use the BackgroundWorker component's ReportProgress method (instead of Control.BeginInvoke) to transition from the worker thread to the UI thread. ShowProgress is perfectly capable of calculating the progress percentage on its own, so we eschew this argument to ReportProgress in favor of an instance of a new custom type called CalcPiUserState. BackgroundWorker packages the arguments passed to ReportProgress and passes them back to the UI thread. You can access them and respond accordingly by handling BackgroundWorker's ProgressChanged event:

// AsyncCalcPiForm.Designer.cs partial class AsyncCalcPiForm {   ...   System.ComponentModel.BackgroundWorker backgroundWorker;   ...   void InitializeComponent() {     ...     this.backgroundWorker =       new System.ComponentModel.BackgroundWorker();     ...     // backgroundWorker     this.backgroundWorker.ProgressChanged +=       backgroundWorker_ProgressChanged;     ...   }  }  // AsyncCalcPiForm.cs  partial class AsyncCalcPiForm : Form {    ...    void ShowProgress(string pi, int totalDigits, int digitsSoFar) {       ...    }    void backgroundWorker_ProgressChanged(      object sender, ProgressChangedEventArgs e) {      // Show progress      CalcPiUserState progress = (CalcPiUserState)e.UserState;      ShowProgress(         progress.Pi, progress.TotalDigits, progress.DigitsSoFar);    } }


The results of bringing a BackgroundWorker into things are shown in Figure 18.10.

Figure 18.10. Safely Displaying Progress Information on the UI Thread


At this point, we've replaced two thingsour use of a custom delegate to start a worker thread and our use of a custom delegate to communicate progress to the UI threadwith two event handlers provided by the BackgroundWorker component, simplifying our code to the following:

partial class AsyncCalcPiForm : Form {    public AsyncCalcPiForm() {      InitializeComponent();    }    void ShowProgress(string pi, int totalDigits, int digitsSoFar) {   // Make sure we're on the UI thread   Debug.Assert(this.InvokeRequired == false);   if(this.InvokeRequired == true ) throw new Exception("Doh!");   // Display progress in UI   this.resultsTextBox.Text = pi;   this.calcToolStripProgressBar.Maximum = totalDigits;   this.calcToolStripProgressBar.Value = digitsSoFar;   if( digitsSoFar == totalDigits ) {     // Reset progress UI     this.calcToolStripStatusLabel.Text = "Ready";     this.calcToolStripProgressBar.Visible = false;   } } class CalcPiUserState {   public readonly string Pi;   public readonly int TotalDigits;   public readonly int DigitsSoFar;   public CalcPiUserState(     string pi, int totalDigits, int digitsSoFar) {     this.Pi = pi;     this.TotalDigits = totalDigits;     this.DigitsSoFar = digitsSoFar;   } } void CalcPi(int digits) {   StringBuilder pi = new StringBuilder("3", digits + 2);   // Report initial progress   this.backgroundWorker.ReportProgress(0,     new CalcPiUserState(pi.ToString(), digits, 0));   if( digits > 0 ) {     pi.Append(".");     for( int i = 0; i < digits; i += 9 ) {       int nineDigits = NineDigitsOfPi.StartingAt(i + 1);       int digitCount = Math.Min(digits - i, 9);       string ds = string.Format("{0:D9}", nineDigits);       pi.Append(ds.Substring(0, digitCount));       // Report continuing progress       this.backgroundWorker.ReportProgress(0,        new CalcPiUserState(pi.ToString(), digits, i + digitCount));     }   } } void calcButton_Click(object sender, EventArgs e) {   // Set calculating UI   this.calcToolStripProgressBar.Visible = true;   this.calcToolStripStatusLabel.Text = "Calculating...";   // Begin calculating pi asynchronously   this.backgroundWorker.RunWorkerAsync(     (int)this.decimalPlacesNumericUpDown.Value); } void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {   CalcPi((int)e.Argument); } void backgroundWorker_ProgressChanged(   object sender, ProgressChangedEventArgs e) {   // Show progress   CalcPiUserState progress = (CalcPiUserState)e.UserState;   ShowProgress(     progress.Pi, progress.TotalDigits, progress.DigitsSoFar);  } }


When the number of digits calculated equals the requested number of digits to be calculated in ShowProgress, we intuit that our long-running operation is complete and reset the UI appropriately. However, we'd like to be able to know when DoWork is completed more generally, as we did in the asynchronous delegate case earlier. Further, we'd like the notification of completion to occur on the UI thread to avoid the need to transition manually, as we had to do before.

Completion

When a BackgroundWorker-managed worker thread completes, BackgroundWorker fires the RunWorkerCompleted event. This allows us to refactor our ShowProgress method and let RunWorkerCompleted reset the status strip progress bar state:

void ShowProgress(string pi, int totalDigits, int digitsSoFar) {    // Make sure we're on the UI thread    Debug.Assert(this.InvokeRequired == false);    if(this.InvokeRequired == true ) throw new Exception("Doh!");    // Display progress in UI    this.resultsTextBox.Text = pi;    this.calcToolStripProgressBar.Maximum = totalDigits;    this.calcToolStripProgressBar.Value = digitsSoFar;    // No need to check for digitsSoFar == totalDigits  } void backgroundWorker_RunWorkerCompleted(   object sender, RunWorkerCompletedEventArgs e) {   // Reset progress UI   this.calcToolStripStatusLabel.Text = "Ready";   this.calcToolStripProgressBar.Visible = false; }


This is the simplest possible completion logic you'll find. A more complex scenario might require you to pass some information from the DoWork event at the end of background operation processing. For example, the pi calculator might want to know the calculation's elapsed time. To do this requires calculating it from the DoWork method and returning it to the RunWorkerCompleted event handler. DoWork's DoWorkEventArgs exposes a Result property that you can set to return a value from the worker thread when complete:

void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {   // Track start time   DateTime start = DateTime.Now;   CalcPi((int)e.Argument);   // Return elapsed time   DateTime end = DateTime.Now;   TimeSpan elapsed = end - start;   e.Result = elapsed; }


The returned value can be accessed from the suitably named Result property exposed by the RunWorkerCompletedEventArgs object passed to RunWorkerCompleted:

void backgroundWorker_RunWorkerCompleted(    object sender, RunWorkerCompletedEventArgs e) {    ...    // Show elapsed time    TimeSpan elapsed = (TimeSpan)e.Result;    MessageBox.Show("Elapsed: " + elapsed.ToString()); }


Figure 18.11 shows the result.

Figure 18.11. Result of Passing a Result Value from DoWork to RunWorkerCompleted


All's well that ends well. Well, almost. Not every background operation ends nicely. For example, it is possible that DoWork will throw an exception and end prematurely. In these cases, you won't notice unless you handle the RunWorkerCompleted event and inspect the Result property of RunWorkerCompletedEventArgs. If you don't wrap the access to the Result property in a try-catch handler, your users will see something like Figure 18.12 in the event of an exception on the worker thread.

Figure 18.12. Exception Raised from the RunWorkerCompleted Event Handler


One good reason to handle the RunWorkerCompleted event is to check for and respond appropriately to exceptions raised on the worker thread. In fact, if you'd like to catch the exception on the UI thread thrown from the worker thread, you should wrap your access to the Result property of the RunWorkerCompletedEventArgs in a try-catch block. If you prefer to avoid the exception altogether or if you just don't need anything from the Result property RunWorkerEventArgs provides, an Error property:

void backgroundWorker_RunWorkerCompleted(    object sender, RunWorkerCompletedEventArgs e) {    // Was there an error?    if( e.Error != null ) {      this.resultsTextBox.Text = e.Error.Message;      return;   }   ... }


Figure 18.13 illustrates a more graceful result.

Figure 18.13. Result of Nicely Handling an Exception in DoWork


Cancellation

In the case of an exception on the worker thread, our pi calculation will be prematurely aborted. However, what if the user wants to cancel it? Maybe the user only wants 100,000 digits after mistakenly asking for 100,001. Figure 18.14 shows an updated CalcPi UI that allows cancellation.

Figure 18.14. Letting the User Cancel a Long-Running Operation


Implementing cancellation for a long-running operation is a multistep process. First, we provide a UI so that users can cancel the operation. In this case, the Calculate button changes to a Cancel button after the calculation begins. Another popular choice is to open a separate progress dialog to display current progress details, typically via a progress bar that shows the percentage of completed work, and a Cancel button.

Second, if the user decides to cancel, the UI should be disabled for the short amount of time that elapses between the time that the UI thread knows the worker thread should stop and the time that the worker thread itself knows and has a chance to stop. If this period of time is ignored, it's possible that the user could start another operation before the first worker thread stops sending progress, making it the job of the UI thread to figure out whether it's getting progress from the new worker thread or the old worker thread that's supposed to be shutting down.

Luckily, BackgroundWorker itself provides all the information we need to implement our cancellation UI:

void calcButton_Click(object sender, EventArgs e) {    // Don't process if cancel request pending    // (Should not be called, because we disabled the button...)    if( this.backgroundWorker.CancellationPending ) return;    // If worker thread currently executing, cancel it    if( this.backgroundWorker.IsBusy ) {      this.calcButton.Enabled = false;      this.backgroundWorker.CancelAsync();      return;    }    // Set calculating UI    this.calcButton.Text = "Cancel";    this.calcToolStripProgressBar.Visible = true;    this.calcToolStripStatusLabel.Text = "Calculating...";    // Begin calculating pi asynchronously    this.backgroundWorker.RunWorkerAsync(      (int)this.decimalPlacesNumericUpDown.Value); }


Here, we use the CancellationPending property of BackgroundWorker to find out whether we've already canceled the pi calculation, in which case we're stuck until the worker thread notices (more on that later).

If there's no cancellation pending, we check the IsBusy property to determine whether the BackgroundWorker is currently executing. If so, it means that the user has pressed the Cancel button. In that case, we disable the Cancel button to let the user know we're working on it, and we invoke CancelAsync to instruct BackgroundWorker to cancel executing.

Finally, if cancellation isn't pending and if BackgroundWorker isn't busy, it means that the user pressed the Calculate button, so we change the text to Cancel and start the calculation process.

When the calculation has started, CancelAsync is actually only a request, so the worker thread needs to watch for it by checking the BackgroundWorker component's CancellationPending property:

void CalcPi(int digits) {    StringBuilder pi = new StringBuilder("3", digits + 2);    // Report initial progress    this.backgroundWorker.ReportProgress(0,      new CalcPiUserState(pi.ToString(), digits, 0));    if( digits > 0 ) {      pi.Append(".");      for( int i = 0; i < digits; i += 9 ) {        int nineDigits = NineDigitsOfPi.StartingAt(i + 1);        int digitCount = Math.Min(digits - i, 9);        string ds = string.Format("{0:D9}", nineDigits);        pi.Append(ds.Substring(0, digitCount));        // Report continuing progress        this.backgroundWorker.ReportProgress(0,          new CalcPiUserState(pi.ToString(), digits, i + digitCount));        // Check for cancellation        if( this.backgroundWorker.CancellationPending ) return;      }    } }


Although you can simply return if CancellationPending is true, you should also set DoWorkEventArg's Cancel property to true; in this way, you can detect whether the long-running operation was canceled from the RunWorkerComplete event handler by inspecting the Cancelled property exposed by RunWorkerCompletedEventArgs:

void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {    ...    CalcPi((int)e.Argument);    // Indicate cancellation    if( this.backgroundWorker.CancellationPending ) {       e.Cancel = true;    }    ...  }  void backgroundWorker_RunWorkerCompleted(    object sender, RunWorkerCompletedEventArgs e) {    ...    // Was the worker thread canceled?    if( e.Cancelled ) {       this.resultsTextBox.Text = "Canceled";       return;    }    ... }


BackgroundWorker Work Flow

DoWork, ProgressChanged, and RunWorkerCompleted comprise the three events you can handle for BackgroundWorker, with at least DoWork being fired when RunWorkerAsync is invoked from the UI thread. Figure 18.15 illustrates the overall work flow, including optional cancellation.

Figure 18.15. BackgroundWorker Work Flow


Shared Data

Thus far, we've been passing around data copies or data ownership. For example, consider the case where we pass the desired number of digits of pi into the worker thread. Because we pass an integer, the worker thread receives its own copy of that data. On the other hand, when we pass an instance of the CalcPiUserState object from the worker thread to the UI thread, we're passing ownership of that data; in other words, the worker thread creates the object but no longer touches it after passing it to the UI thread:

void CalcPi(int digits) {   ...   // Pass ownership of the CalcPiUserState from the worker to the UI  this.backgroundWorker.ReportProgress(0,    new CalcPiUserState(pi.ToString(), digits, i + digitCount));  ... }


By passing copies or ownership of data, we avoid a situation where multiple threads share simultaneous access to data. For example, suppose we decide that we prefer shared access to an object that holds state associated with the pi calculation:

class SharedCalcPiUserState {    public string Pi;    public int TotalDigits;    public int DigitsSoFar;  }  SharedCalcPiUserState state = new SharedCalcPiUserState();  void CalcPi(int digits) {    ...    // Update state and notify UI    this.state.Pi = pi.ToString();    this.state.TotalDigits = digits;    this.state.DigitsSoFar = i + digitCount;    this.backgroundWorker.ReportProgress(0);    ... }


Here, the UI thread is free to access the shared state data while the worker thread continues its calculation:

void backgroundWorker_ProgressChanged(    object sender, ProgressChangedEventArgs e) {    // Show progress    ShowProgress(       this.state.Pi, this.state.TotalDigits, this.state.DigitsSoFar); }


I hope that something inside you cringes when you look at this code.

If you're going to do multithreaded programming, you must watch out for situations where two threads have simultaneous access to the same data. Shared access to data between threads makes it very easy to get into race conditions, in which one thread is racing to read data that is only partially up-to-date before another thread has finished updating it. In this example, it's completely possible to be forming a call stack to the ShowProgress method on the UI thread while the worker thread continues to update the values in the background, causing you to pass values from the SharedCalcPiUserState class from as many as three different iterations of the worker thread.

For proper concurrent access to shared data, you must synchronize access to the datathat is, make sure that one thread waits patiently while another thread works on the data. To synchronize access to shared data, C# provides the lock block:

SharedCalcPiUserState state = new SharedCalcPiUserState(); object stateLock = new object(); void CalcPi(int digits) {   ...   // Synchronize access to shared data   // on the worker thread   lock( stateLock ) {     this.state.Pi = pi.ToString();     this.state.TotalDigits = digits;     this.state.DigitsSoFar = i + digitCount;     this.backgroundWorker.ReportProgress(0);   }   ... } void backgroundWorker_ProgressChanged(   object sender, ProgressChangedEventArgs e) {   // Synchronize access to shared data   // on the UI thread   lock( stateLock ) {      ShowProgress(         this.state.Pi, this.state.TotalDigits, this.state.DigitsSoFar);   } }


Now that your data has been properly protected against race conditions, you must watch out for another problem known as a deadlock. A deadlock occurs when each of two threads has locked a resource and both subsequently wait for the resource held by the other thread, causing each thread to stop dead, waiting forever. When two threads are deadlocked, each of them waits for the other to complete its work before continuing, thereby ensuring that neither actually progresses.

If all this talk of race conditions and deadlocks has caused you concern, that's good. Can you look at the CalcPi method and the ProgressChanged event handler and know for sure that we haven't introduced a deadlock, or even that we have solved our race condition properly?

Multithreaded programming with shared data is hard. By passing copies or ownership of data around, we ensure that no two threads need to share access to any one piece of data. If you don't have shared data, there's no need to synchronize access to it. But if you find that you need access to shared datamaybe because the overhead of copying the data is too great a burden in space or timethen you need to read up on multithreading and shared data synchronization, topics that are beyond the scope of this book.

Luckily, the vast majority of multithreading scenarios, especially as related to UI multithreading, seem to work best with the simple passing of copies or ownership of data, an approach we've employed in our CalcPi example.




Windows Forms 2.0 Programming
Windows Forms 2.0 Programming (Microsoft .NET Development Series)
ISBN: 0321267966
EAN: 2147483647
Year: 2006
Pages: 216

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net