Udostępnij za pośrednictwem


StackOverflow answer – why learn multi-core programming? #2

In my last post I talked about how multi-threading can be used to improve responsiveness while loading a file.    I’ve put together a sample program (source included) that shows how to do this in C# 3.0, .NET 3.5, and WPF using Visual Studio 2008.  You can find a zip file with a complete project attached.

The program has a very simple, but fully functional UI.

BFE1

The “Open a file…” pops up the standard open file dialog and ask the user to find a file.   The cancel button is disabled unless there is a loading operation to cancel.  The text box is for messages from the loader thread and the slider is just there to show that the UI is responsive while the background load occurs.

BFE2

The progress bar is hidden unless a file is actually being loaded.  If the async check box is un-checked, then the file will be loaded synchronously.  This illustrates how the UI will ‘freeze’ if a second thread isn’t used.

The key thing about this program is that the UI stays completely responsive while a file is loaded.  Of course, this can be done without threads – many programs are written using a state machine approach.  But using threads, and functional programming make this much more simple than previous approaches.

There are many ways to leverage threads in managed code, but the most straight forward way is using the BackgroundWorker class.  IT can be used in two ways, by creating one and attaching functions to its delegates, or by creating a new class derived from BackgroundWorker – this is the approach I took here.  

I created a class called AsyncFileLoader that is derived from BackgroundWorker.  We’re using all the features of BackgroundWorker here

  1. Support for asynchronous progress notifications
  2. Support for cancelation
  3. Handling of errors during the file loading

Progress and cancelation is enabled by setting their properties in the constructor.

    1: public AsyncFileLoader()
    2: {
    3:     WorkerReportsProgress = true;
    4:     WorkerSupportsCancellation = true;
    5: }

We also need an event handler that will be connected to the cancel button

    1: public void PerformCancel( object sender,  RoutedEventArgs e )
    2: {
    3:     this.CancelAsync();
    4: }

The final element is a function that overrides BackgroundWorker.OnDoWork().   This function does all the work of asynchronously loading a text file.

    1: protected override void OnDoWork( DoWorkEventArgs e )
    2: {
    3:     // note: do not catch exceptions here.  The BackgroundWorker will catch exceptions 
    4:     // and pass them back to the initiating thread in the RunWorkerCompleted() delegate 
    5:     // using the error member of RunWorkerCompletedEventArgs parameter.
    6:  
    7:     string FileName = e.Argument as string;
    8:  
    9:     ReportProgress( int.MinValue, String.Format( "Opening {0}", FileName ) );
   10:  
   11:     using ( StreamReader sr = new StreamReader( FileName, Encoding.UTF8 ) ) 
   12:     {
   13:         ReportProgress( int.MinValue, String.Format( "Loading {0}", FileName ) );
   14:  
   15:         long FileLength = sr.BaseStream.Length;
   16:         long PercentRead = 0;
   17:         long LastPercentRead = 0;
   18:  
   19:         String line;
   20:  
   21:         while ( (line = sr.ReadLine()) != null ) {
   22:  
   23:             if ( CancellationPending ) {
   24:                 e.Cancel = true;
   25:                 break;
   26:             }
   27:  
   28:             PercentRead = (sr.BaseStream.Position * 100) / FileLength;
   29:  
   30:             if ( PercentRead >= LastPercentRead + 1 ) {
   31:                 ReportProgress( (int)PercentRead );
   32:                 LastPercentRead = PercentRead;
   33:             }
   34:         }
   35:     }
   36: }

As you can see – this is really simple, and most important procedural – there isn’t a state machine, or any explicitly state kept at all.  This single function loads the file, and sends progress messages and errors asynchronously to the calling thread.

The initiating thread passes the background thread parameters using the DoWorkEventArgs class.  Here, I’ve simply passed the file name as a string, but any class or structure could be passed this way.

Line 9 is important – it asynchronously passes the first progress message back to the main thread.   This includes the progress percentage and the name of the file.  Here I’m using int.MinValue to differentiate the first progress message from subsequent ones.  This sentinel value means that the main thread doesn’t have to maintain any state to handle progress messages.

I mentioned above that this function handles errors, but you don’t see any error handling code!  Any errors that occur from opening the file, or reading the data are signaled as exceptions.  The BackgroundWorker class will catch these and pass them back tot he main thread.  I just love this – it keeps the code here very clean.

Note the using statement on line 11.  It is important to do this when using file I/O objects so the underlying OS objects get cleaned up correctly.  See this for more information.

The progress percentage value is computed very simply, the code simply keeps track of the file position as a ration to the file size.  This is the obvious part.

Note line 30 – this is very important.  It only sends progress messages to the main thread when the value changes – not for every line, which would be quite expensive (give it a try to see).

The entire AsyncFileLoader.cs file is not very big, only 29 lines of actual code and is very straightforward.

The code in the main thread to mange the UI is equally straight forward and consists of just a single event handler attached to the open button’s click delegate.  This one function does all of the following:

  1. Gets the file name from the user using the open file dialog.
  2. Maintains the UI state, disable buttons,  showing/hiding the progress bar, displaying messages, etc.
  3. Creates a the AsyncFileLoader object
  4. Starts the asynchronous file loading operation
  5. Handles progress messages
  6. handles the completion of the operation
  7. Handles errors.
    1: private void OpenAFile_Clicked( object sender, RoutedEventArgs e )
    2: {
    3:     try {
    4:         OpenFileDialog ofd = new OpenFileDialog();
    5:         ofd.CheckFileExists = true;
    6:         ofd.DefaultExt = "txt";
    7:         ofd.Title = "Open an text File";
    8:         ofd.Filter = "Text|*.txt";
    9:  
   10:         OpenAFile_Button.IsEnabled = false;
   11:  
   12:         ofd.ShowDialog();
   13:  
   14:         if ( ofd.FileName.IsBlank() ) {
   15:             OpenAFile_Button.IsEnabled = true;
   16:             return;
   17:         }
   18:  
   19:         AsyncFileLoader TheLoader = new AsyncFileLoader();
   20:  
   21:         //---- Do this when the progress bar is updated
   22:         TheLoader.ProgressChanged += ( Sender, ArgsX ) => {
   23:  
   24:             if ( ArgsX.ProgressPercentage == int.MinValue ) {
   25:                 Cancel_Button.IsEnabled = true;
   26:                 Cancel_Button.Click += TheLoader.PerformCancel;
   27:                 OpenProgress_ProgressBar.IsEnabled = true;
   28:                 OpenProgress_ProgressBar.Visibility = Visibility.Visible;
   29:                 OpenProgress_ProgressBar.Value = 0;
   30:                 Info_TextBox.Text = ArgsX.UserState as String;
   31:             }
   32:             else {
   33:                 OpenProgress_ProgressBar.Value = ArgsX.ProgressPercentage;
   34:             }
   35:         };
   36:  
   37:         //---- Do this when the background worker is completed 
   38:         TheLoader.RunWorkerCompleted += ( SenderX, ArgsX ) => {
   39:  
   40:             RunWorkerCompletedEventArgs args = ArgsX as RunWorkerCompletedEventArgs;
   41:  
   42:             if ( args.Error != null ) {
   43:                 PopErrorDialog( args.Error, "Cannot process file!" );
   44:             }
   45:             else
   46:             if ( args.Cancelled ) {
   47:                 Info_TextBox.Text = "Loading cancled";
   48:             }
   49:             else {
   50:                 Info_TextBox.Text = "File Loaded";
   51:             }
   52:  
   53:             Cancel_Button.IsEnabled = false;
   54:             Cancel_Button.Click -= TheLoader.PerformCancel;
   55:             OpenProgress_ProgressBar.IsEnabled = false;
   56:             OpenProgress_ProgressBar.Visibility = Visibility.Hidden;
   57:             OpenAFile_Button.IsEnabled = true;
   58:         };
   59:  
   60:         //---- Start the ascynrnous file loading operation
   61:         TheLoader.RunWorkerAsync( ofd.FileName );
   62:     }
   63:     catch ( Exception Ex ) {
   64:         PopErrorDialog( Ex, "General Failure!" );
   65:     }
   66: }

Functional programming is key to making this code clean and straight forward.   Here we’re using lambda functions connected to the AsyncFileLoader’s progress and completion events.

You can see in the progress changed handler how no state needs to be kept – this lambda function gets all the data it needs from the event.   You can see how the sentinel value (int.MinValue) is used to trigger the change in UI state.

Completion is handles in a similarly manner and resets the UI state.   This lambda also handles any errors thrown by the loader by checking the args.Error member which will reference an exception if one was thrown.

This is a great example of using multi-threaded programming for something other than speeding up a computationally bound algorithm using parallelization.   Some may suggest that this isn’t “real multi-core” programming, but it is.  Indeed, the developer doesn’t need to know how to implement complex inter thread communication or synchronization, but just because this isn’t hard, doesn’t mean it isn’t real multi-threaded, and multi-core enabled code.  It absolutely is and the developer does need to understand asynchronous programming techniques to use it.

Using the BackgroundWorker class is a very simple straight forward way to enable your programs to be more responsive, and to leverage multi-threaded and multi-core performance.   You can use other managed code techniques such as threads and dispatching for more sophisticated work.  

del.icio.us Tags: performacne,programming,C#,multi-core,multicore

You can find the complete VS 2008 project in the attached ZIP file.

BackgroundFileOpenExample.zip

Comments

  • Anonymous
    August 06, 2009
    Nice, simple example for beginning programmers. I'm curious though as to why in the world you would create a class that inherits from BackgroundWorker instead of just using BackgroundWorker directly?