Background Worker Pattern

Frequently with multithreaded operations, you not only want to be notified when the thread completes, but you also want the method to provide an update on the status of the operation. Often, users want to be able to cancel long-running tasks. The .NET Framework 2.0 includes a BackgroundWorker class for programming this type of pattern.

Listing 16.8 is an example of this pattern. It calculates pi to the number of digits specified.

Listing 16.8. Using the Background Worker Pattern

 using System; using System.Threading; using System.ComponentModel; using System.Text; public class PiCalculator {         public static BackgroundWorker calculationWorker =                                 new BackgroundWorker();                                                   public static AutoResetEvent resetEvent =             new AutoResetEvent(false);         public static void Main()         {             int digitCount;             Console.Write(                         "Enter the number of digits to calculate:");             if (int.TryParse(Console.ReadLine(), out digitCount))             {                  Console.WriteLine("ENTER to cancel");                  // C# 2.0 Syntax for registering delegates                  calculationWorker.DoWork += CalculatePi;                             // Register the ProgressChanged callback                             calculationWorker.ProgressChanged+=                                       UpdateDisplayWithMoreDigits;                                   calculationWorker.WorkerReportsProgress = true;                       // Register a callback for when the                                  // calculation completes                                            calculationWorker.RunWorkerCompleted +=                                     new RunWorkerCompletedEventHandler(Complete);                 calculationWorker.WorkerSupportsCancellation = true;                  // Begin calculating pi for up to digitCount digits                  calculationWorker.RunWorkerAsync(digitCount);                        Console.ReadLine();                                                  // If cancel is called after the calculation                         // has completed it doesn't matter.                                  calculationWorker.CancelAsync();                                     // Wait for Complete() to run.                                      resetEvent.WaitOne();              }              else              {                 Console.WriteLine(                         "The value entered is an invalid integer.");              }        }        private static void CalculatePi(                object sender, DoWorkEventArgs eventArgs)     {        int digits = (int)eventArgs.Argument;        StringBuilder pi = new StringBuilder("3.", digits + 2);        calculationWorker.ReportProgress(0, pi.ToString());        // Calculate rest of pi, if required        if (digits > 0)        {             for (int i = 0; i < digits; i += 9)             {                 // Calculate next i decimal places                 int nextDigit = PiDigitCalculator.StartingAt(                       i + 1);                 int digitCount = Math.Min(digits - i, 9);                 string ds = string.Format("{0:D9}", nextDigit);                 pi.Append(ds.Substring(0, digitCount));                        // Show current progress                 calculationWorker.ReportProgress(                        0, ds.Substring(0, digitCount));                 // Check for cancellation                 if (calculationWorker.CancellationPending)                 {                     // Need to set Cancel if you need to                     // distinguish how a worker thread completed                     // i.e., by checking                     // RunWorkerCompletedEventArgs.Cancelled                     eventArgs.Cancel = true;                     break;                 }             }         }         eventArgs.Result = pi.ToString();      }      private static void UpdateDisplayWithMoreDigits(         object sender, ProgressChangedEventArgs eventArgs)      {         string digits = (string)eventArgs.UserState;      Console.Write(digits);      }      static void Complete(          object sender, RunWorkerCompletedEventArgs eventArgs)      {          // ...      }    } ----------------------------------------------------------------------------- -----------------------------------------------------------------------------    public class PiDigitCalculator    {       // ...    } 

Establishing the Pattern

The process of hooking up the background worker pattern is as follows.

  1. Register the long-running method with the BackgroundWorker.DoWork event. In this example, the long-running task is the call to CalculatePi().

  2. To receive progress or status notifications, hook up a listener to BackgroundWorker.ProgressChanged and set BackgroundWorker.WorkerReportsProgress to true. In Listing 16.8, the UpdateDisplayWithMoreDigits() method takes care of updating the display as more digits become available.

  3. Register a method (Complete()) with the BackgroundWorker.RunWorkerCompleted event.

  4. Assign the WorkerSupportsCancellation property to support cancellation. Once this property is assigned the value true, a call to BackgroundWorker.CancelAsync will set the DoWorkEventArgs.CancellationPending flag.

  5. Within the DoWork-provided method (CalculatePi()), check the DoWorkEventArgs.CancellationPending property and exit the method when it is true.

  6. Once everything is set up, you can start the work by calling BackgroundWorker.RunWorkerAsync() and providing a state parameter that is passed to the specified DoWork() method.

When it is broken down into steps, the background worker pattern is relatively easy to follow and provides the advantage over the asynchronous results pattern of a mechanism for cancellation and progress notification. The drawback is that you cannot use it arbitrarily on any method. Instead, the DoWork() method has to conform to a System.ComponentModel. DoWorkEventHandler delegate, which takes arguments of type object and DoWorkEventArgs. If this isn't the case, then a wrapper function is required. The cancellation- and progress-related methods also require specific signatures, but these are in control of the programmer setting up the background worker pattern.

Exception Handling

If an unhandled exception occurs while the background worker thread is executing, then the RunWorkerCompletedEventArgs parameter of the RunWorkerCompleted' delegate (Completed's eventArgs) will have an Error property set with the exception. As a result, checking the Error property within the RunWorkerCompleted callback in Listing 16.9 provides a means of handling the exception.

Listing 16.9. Handling Unhandled Exceptions from the Worker Thread

   // ...   static void Complete(       object sender, RunWorkerCompletedEventArgs eventArgs)   {       Console.WriteLine();       if (eventArgs.Cancelled)       {               Console.WriteLine("Cancelled");       }       else if (eventArgs.Error!= null)                                           {                                                                                 // IMPORTANT: check error to retrieve any exceptions.                     Console.WriteLine(                    "ERROR: {0}", eventArgs.Error.Message);        }                                                                        else       {           Console.WriteLine("Finished");       }       resetEvent.Set();  } // ... 

It is important that the code check eventArgs.Error inside the RunWorkerCompleted callback. Otherwise, the exception will go undetected; it won't even be reported to AppDomain.

Essential C# 2.0
Essential C# 2.0
ISBN: 0321150775
EAN: 2147483647
Year: 2007
Pages: 185 © 2008-2017.
If you may any questions please contact us: